Merge pull request #329 from Moonlight-Panel/AddStore
Implemented new store system
This commit is contained in:
commit
a671e601c0
100 changed files with 6870 additions and 29 deletions
|
@ -6,6 +6,12 @@
|
|||
<entry key="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="solutionLevelOptions">
|
||||
<map>
|
||||
<entry key="migrationsProject" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
||||
<entry key="startupProject" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="startupToMigrationsProjects">
|
||||
<map>
|
||||
<entry key="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
||||
|
|
22
Moonlight/App/Actions/Dummy/DummyActions.cs
Normal file
22
Moonlight/App/Actions/Dummy/DummyActions.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Models.Abstractions;
|
||||
|
||||
namespace Moonlight.App.Actions.Dummy;
|
||||
|
||||
public class DummyActions : ServiceActions
|
||||
{
|
||||
public override Task Create(IServiceProvider provider, Service service)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task Update(IServiceProvider provider, Service service)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task Delete(IServiceProvider provider, Service service)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,15 @@ public class ConfigV1
|
|||
[JsonProperty("Security")] public SecurityData Security { get; set; } = new();
|
||||
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
|
||||
[JsonProperty("MailServer")] public MailServerData MailServer { get; set; } = new();
|
||||
|
||||
[JsonProperty("Store")] public StoreData Store { get; set; } = new();
|
||||
|
||||
public class StoreData
|
||||
{
|
||||
[JsonProperty("Currency")]
|
||||
[Description("A string value representing the currency which will be shown to a user")]
|
||||
public string Currency { get; set; } = "€";
|
||||
}
|
||||
|
||||
public class SecurityData
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Database.Entities.Tickets;
|
||||
using Moonlight.App.Services;
|
||||
|
||||
|
@ -12,6 +13,18 @@ public class DataContext : DbContext
|
|||
public DbSet<User> Users { get; set; }
|
||||
//public DbSet<Ticket> Tickets { get; set; }
|
||||
//public DbSet<TicketMessage> TicketMessages { get; set; }
|
||||
|
||||
// Store
|
||||
public DbSet<Category> Categories { get; set; }
|
||||
public DbSet<Product> Products { get; set; }
|
||||
public DbSet<Service> Services { get; set; }
|
||||
public DbSet<ServiceShare> ServiceShares { get; set; }
|
||||
|
||||
public DbSet<GiftCode> GiftCodes { get; set; }
|
||||
public DbSet<GiftCodeUse> GiftCodeUses { get; set; }
|
||||
|
||||
public DbSet<Coupon> Coupons { get; set; }
|
||||
public DbSet<CouponUse> CouponUses { get; set; }
|
||||
|
||||
public DataContext(ConfigService configService)
|
||||
{
|
||||
|
|
9
Moonlight/App/Database/Entities/Store/Category.cs
Normal file
9
Moonlight/App/Database/Entities/Store/Category.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Moonlight.App.Database.Entities.Store;
|
||||
|
||||
public class Category
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string Slug { get; set; } = "";
|
||||
}
|
9
Moonlight/App/Database/Entities/Store/Coupon.cs
Normal file
9
Moonlight/App/Database/Entities/Store/Coupon.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Moonlight.App.Database.Entities.Store;
|
||||
|
||||
public class Coupon
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Code { get; set; } = "";
|
||||
public int Percent { get; set; }
|
||||
public int Amount { get; set; }
|
||||
}
|
7
Moonlight/App/Database/Entities/Store/CouponUse.cs
Normal file
7
Moonlight/App/Database/Entities/Store/CouponUse.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Moonlight.App.Database.Entities.Store;
|
||||
|
||||
public class CouponUse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public Coupon Coupon { get; set; }
|
||||
}
|
9
Moonlight/App/Database/Entities/Store/GiftCode.cs
Normal file
9
Moonlight/App/Database/Entities/Store/GiftCode.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Moonlight.App.Database.Entities.Store;
|
||||
|
||||
public class GiftCode
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Code { get; set; } = "";
|
||||
public double Value { get; set; }
|
||||
public int Amount { get; set; }
|
||||
}
|
7
Moonlight/App/Database/Entities/Store/GiftCodeUse.cs
Normal file
7
Moonlight/App/Database/Entities/Store/GiftCodeUse.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Moonlight.App.Database.Entities.Store;
|
||||
|
||||
public class GiftCodeUse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public GiftCode GiftCode { get; set; }
|
||||
}
|
23
Moonlight/App/Database/Entities/Store/Product.cs
Normal file
23
Moonlight/App/Database/Entities/Store/Product.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using Moonlight.App.Database.Enums;
|
||||
|
||||
namespace Moonlight.App.Database.Entities.Store;
|
||||
|
||||
public class Product
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public Category Category { get; set; }
|
||||
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string Slug { get; set; } = "";
|
||||
|
||||
public double Price { get; set; }
|
||||
public int Stock { get; set; }
|
||||
public int MaxPerUser { get; set; }
|
||||
public int Duration { get; set; }
|
||||
|
||||
public ServiceType Type { get; set; }
|
||||
public string ConfigJson { get; set; } = "{}";
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
18
Moonlight/App/Database/Entities/Store/Service.cs
Normal file
18
Moonlight/App/Database/Entities/Store/Service.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace Moonlight.App.Database.Entities.Store;
|
||||
|
||||
public class Service
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? Nickname { get; set; }
|
||||
|
||||
public bool Suspended { get; set; } = false;
|
||||
|
||||
public Product Product { get; set; }
|
||||
public string? ConfigJsonOverride { get; set; }
|
||||
|
||||
public User Owner { get; set; }
|
||||
public List<ServiceShare> Shares { get; set; } = new();
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime RenewAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
7
Moonlight/App/Database/Entities/Store/ServiceShare.cs
Normal file
7
Moonlight/App/Database/Entities/Store/ServiceShare.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Moonlight.App.Database.Entities.Store;
|
||||
|
||||
public class ServiceShare
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public User User { get; set; }
|
||||
}
|
8
Moonlight/App/Database/Entities/Store/Transaction.cs
Normal file
8
Moonlight/App/Database/Entities/Store/Transaction.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Moonlight.App.Database.Entities.Store;
|
||||
|
||||
public class Transaction
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public double Price { get; set; }
|
||||
public string Text { get; set; } = "";
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
namespace Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
|
||||
namespace Moonlight.App.Database.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
|
@ -9,6 +11,13 @@ public class User
|
|||
public string? Avatar { get; set; } = null;
|
||||
public string? TotpKey { get; set; } = null;
|
||||
|
||||
// Store
|
||||
public double Balance { get; set; }
|
||||
public List<Transaction> Transactions { get; set; } = new();
|
||||
|
||||
public List<CouponUse> CouponUses { get; set; } = new();
|
||||
public List<GiftCodeUse> GiftCodeUses { get; set; } = new();
|
||||
|
||||
// Meta data
|
||||
public string Flags { get; set; } = "";
|
||||
public int Permissions { get; set; } = 0;
|
||||
|
|
9
Moonlight/App/Database/Enums/ServiceType.cs
Normal file
9
Moonlight/App/Database/Enums/ServiceType.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Moonlight.App.Database.Enums;
|
||||
|
||||
public enum ServiceType
|
||||
{
|
||||
Server,
|
||||
Webspace,
|
||||
Database,
|
||||
Domain
|
||||
}
|
341
Moonlight/App/Database/Migrations/20231017075519_AddStoreModels.Designer.cs
generated
Normal file
341
Moonlight/App/Database/Migrations/20231017075519_AddStoreModels.Designer.cs
generated
Normal file
|
@ -0,0 +1,341 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Moonlight.App.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20231017075519_AddStoreModels")]
|
||||
partial class AddStoreModels
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Categories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Percent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Coupons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CouponId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CouponId");
|
||||
|
||||
b.ToTable("CouponUses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Value")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("GiftCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GiftCodeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GiftCodeId");
|
||||
|
||||
b.ToTable("GiftCodeUses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConfigJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Duration")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxPerUser")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Price")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Stock")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Products");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConfigJsonOverride")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("OwnerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProductId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("RenewAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Suspended")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ServiceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ServiceShares");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Flags")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Permissions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("TokenValidTimestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TotpKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
|
||||
.WithMany()
|
||||
.HasForeignKey("CouponId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Coupon");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode")
|
||||
.WithMany()
|
||||
.HasForeignKey("GiftCodeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("GiftCode");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId");
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Owner");
|
||||
|
||||
b.Navigation("Product");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Service", null)
|
||||
.WithMany("Shares")
|
||||
.HasForeignKey("ServiceId");
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.Navigation("Shares");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStoreModels : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Coupons",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Code = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Percent = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Amount = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Coupons", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GiftCodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Code = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<double>(type: "REAL", nullable: false),
|
||||
Amount = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GiftCodes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Products",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
CategoryId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Price = table.Column<double>(type: "REAL", nullable: false),
|
||||
Stock = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
MaxPerUser = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Duration = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ConfigJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Products", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Products_Categories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CouponUses",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
CouponId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CouponUses", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CouponUses_Coupons_CouponId",
|
||||
column: x => x.CouponId,
|
||||
principalTable: "Coupons",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GiftCodeUses",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GiftCodeId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GiftCodeUses", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_GiftCodeUses_GiftCodes_GiftCodeId",
|
||||
column: x => x.GiftCodeId,
|
||||
principalTable: "GiftCodes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Services",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Nickname = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Suspended = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ProductId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ConfigJsonOverride = table.Column<string>(type: "TEXT", nullable: true),
|
||||
OwnerId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
RenewAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Services", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Services_Products_ProductId",
|
||||
column: x => x.ProductId,
|
||||
principalTable: "Products",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Services_Users_OwnerId",
|
||||
column: x => x.OwnerId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ServiceShares",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UserId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ServiceId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ServiceShares", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ServiceShares_Services_ServiceId",
|
||||
column: x => x.ServiceId,
|
||||
principalTable: "Services",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_ServiceShares_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CouponUses_CouponId",
|
||||
table: "CouponUses",
|
||||
column: "CouponId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GiftCodeUses_GiftCodeId",
|
||||
table: "GiftCodeUses",
|
||||
column: "GiftCodeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Products_CategoryId",
|
||||
table: "Products",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Services_OwnerId",
|
||||
table: "Services",
|
||||
column: "OwnerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Services_ProductId",
|
||||
table: "Services",
|
||||
column: "ProductId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServiceShares_ServiceId",
|
||||
table: "ServiceShares",
|
||||
column: "ServiceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServiceShares_UserId",
|
||||
table: "ServiceShares",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CouponUses");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "GiftCodeUses");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ServiceShares");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Coupons");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "GiftCodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Services");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Products");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories");
|
||||
}
|
||||
}
|
||||
}
|
371
Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.Designer.cs
generated
Normal file
371
Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.Designer.cs
generated
Normal file
|
@ -0,0 +1,371 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Moonlight.App.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20231018203522_AddedUserStoreData")]
|
||||
partial class AddedUserStoreData
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Categories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Percent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Coupons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CouponId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CouponId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("CouponUses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Value")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("GiftCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GiftCodeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GiftCodeId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("GiftCodeUses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConfigJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Duration")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxPerUser")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Price")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Stock")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Products");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConfigJsonOverride")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("OwnerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProductId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("RenewAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Suspended")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ServiceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ServiceShares");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Balance")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Flags")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Permissions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("TokenValidTimestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TotpKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
|
||||
.WithMany()
|
||||
.HasForeignKey("CouponId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||
.WithMany("CouponUses")
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("Coupon");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode")
|
||||
.WithMany()
|
||||
.HasForeignKey("GiftCodeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||
.WithMany("GiftCodeUses")
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("GiftCode");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Owner");
|
||||
|
||||
b.Navigation("Product");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Service", null)
|
||||
.WithMany("Shares")
|
||||
.HasForeignKey("ServiceId");
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.Navigation("Shares");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("CouponUses");
|
||||
|
||||
b.Navigation("GiftCodeUses");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedUserStoreData : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Products_Categories_CategoryId",
|
||||
table: "Products");
|
||||
|
||||
migrationBuilder.AddColumn<double>(
|
||||
name: "Balance",
|
||||
table: "Users",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0.0);
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "CategoryId",
|
||||
table: "Products",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "UserId",
|
||||
table: "GiftCodeUses",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "UserId",
|
||||
table: "CouponUses",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GiftCodeUses_UserId",
|
||||
table: "GiftCodeUses",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CouponUses_UserId",
|
||||
table: "CouponUses",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_CouponUses_Users_UserId",
|
||||
table: "CouponUses",
|
||||
column: "UserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GiftCodeUses_Users_UserId",
|
||||
table: "GiftCodeUses",
|
||||
column: "UserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Products_Categories_CategoryId",
|
||||
table: "Products",
|
||||
column: "CategoryId",
|
||||
principalTable: "Categories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_CouponUses_Users_UserId",
|
||||
table: "CouponUses");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GiftCodeUses_Users_UserId",
|
||||
table: "GiftCodeUses");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Products_Categories_CategoryId",
|
||||
table: "Products");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_GiftCodeUses_UserId",
|
||||
table: "GiftCodeUses");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_CouponUses_UserId",
|
||||
table: "CouponUses");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Balance",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId",
|
||||
table: "GiftCodeUses");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId",
|
||||
table: "CouponUses");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "CategoryId",
|
||||
table: "Products",
|
||||
type: "INTEGER",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Products_Categories_CategoryId",
|
||||
table: "Products",
|
||||
column: "CategoryId",
|
||||
principalTable: "Categories",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
403
Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.Designer.cs
generated
Normal file
403
Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.Designer.cs
generated
Normal file
|
@ -0,0 +1,403 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Moonlight.App.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20231018204737_AddedTransactions")]
|
||||
partial class AddedTransactions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Categories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Percent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Coupons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CouponId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CouponId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("CouponUses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Value")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("GiftCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GiftCodeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GiftCodeId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("GiftCodeUses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConfigJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Duration")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxPerUser")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Price")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Stock")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Products");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConfigJsonOverride")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("OwnerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProductId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("RenewAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Suspended")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ServiceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ServiceShares");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("Price")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Transaction");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Balance")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Flags")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Permissions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("TokenValidTimestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TotpKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
|
||||
.WithMany()
|
||||
.HasForeignKey("CouponId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||
.WithMany("CouponUses")
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("Coupon");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode")
|
||||
.WithMany()
|
||||
.HasForeignKey("GiftCodeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||
.WithMany("GiftCodeUses")
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("GiftCode");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Owner");
|
||||
|
||||
b.Navigation("Product");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Service", null)
|
||||
.WithMany("Shares")
|
||||
.HasForeignKey("ServiceId");
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("UserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.Navigation("Shares");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("CouponUses");
|
||||
|
||||
b.Navigation("GiftCodeUses");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedTransactions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Transaction",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Price = table.Column<double>(type: "REAL", nullable: false),
|
||||
Text = table.Column<string>(type: "TEXT", nullable: false),
|
||||
UserId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Transaction", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Transaction_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Transaction_UserId",
|
||||
table: "Transaction",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Transaction");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,6 +17,243 @@ namespace Moonlight.App.Database.Migrations
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Categories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Percent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Coupons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CouponId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CouponId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("CouponUses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Value")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("GiftCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GiftCodeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GiftCodeId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("GiftCodeUses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConfigJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Duration")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxPerUser")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Price")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Stock")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Products");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConfigJsonOverride")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("OwnerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProductId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("RenewAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Suspended")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ServiceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ServiceShares");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("Price")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Transaction");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -26,6 +263,9 @@ namespace Moonlight.App.Database.Migrations
|
|||
b.Property<string>("Avatar")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Balance")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -58,6 +298,102 @@ namespace Moonlight.App.Database.Migrations
|
|||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
|
||||
.WithMany()
|
||||
.HasForeignKey("CouponId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||
.WithMany("CouponUses")
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("Coupon");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode")
|
||||
.WithMany()
|
||||
.HasForeignKey("GiftCodeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||
.WithMany("GiftCodeUses")
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("GiftCode");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Owner");
|
||||
|
||||
b.Navigation("Product");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Store.Service", null)
|
||||
.WithMany("Shares")
|
||||
.HasForeignKey("ServiceId");
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("UserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||
{
|
||||
b.Navigation("Shares");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("CouponUses");
|
||||
|
||||
b.Navigation("GiftCodeUses");
|
||||
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
|
10
Moonlight/App/Event/Args/TransactionCreatedEventArgs.cs
Normal file
10
Moonlight/App/Event/Args/TransactionCreatedEventArgs.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
|
||||
namespace Moonlight.App.Event.Args;
|
||||
|
||||
public class TransactionCreatedEventArgs
|
||||
{
|
||||
public Transaction Transaction { get; set; }
|
||||
public User User { get; set; }
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Event.Args;
|
||||
|
||||
namespace Moonlight.App.Event;
|
||||
|
@ -9,4 +10,6 @@ public class Events
|
|||
public static EventHandler<User> OnUserPasswordChanged;
|
||||
public static EventHandler<User> OnUserTotpSet;
|
||||
public static EventHandler<MailVerificationEventArgs> OnUserMailVerify;
|
||||
public static EventHandler<Service> OnServiceOrdered;
|
||||
public static EventHandler<TransactionCreatedEventArgs> OnTransactionCreated;
|
||||
}
|
8
Moonlight/App/Extensions/Attributes/SelectorAttribute.cs
Normal file
8
Moonlight/App/Extensions/Attributes/SelectorAttribute.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Moonlight.App.Extensions.Attributes;
|
||||
|
||||
public class SelectorAttribute : Attribute
|
||||
{
|
||||
public string SelectorProp { get; set; } = "";
|
||||
public string DisplayProp { get; set; } = "";
|
||||
public bool UseDropdown { get; set; } = false;
|
||||
}
|
12
Moonlight/App/Helpers/ComponentHelper.cs
Normal file
12
Moonlight/App/Helpers/ComponentHelper.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Moonlight.App.Helpers;
|
||||
|
||||
public static class ComponentHelper
|
||||
{
|
||||
public static RenderFragment FromType(Type type) => builder =>
|
||||
{
|
||||
builder.OpenComponent(0, type);
|
||||
builder.CloseComponent();
|
||||
};
|
||||
}
|
|
@ -213,7 +213,7 @@ public static class Formatter
|
|||
return builder =>
|
||||
{
|
||||
int i = 0;
|
||||
var arr = content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var arr = content.Split("\n");
|
||||
|
||||
foreach (var line in arr)
|
||||
{
|
||||
|
|
57
Moonlight/App/Helpers/PropBinder.cs
Normal file
57
Moonlight/App/Helpers/PropBinder.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace Moonlight.App.Helpers;
|
||||
|
||||
public class PropBinder<T>
|
||||
{
|
||||
private PropertyInfo PropertyInfo;
|
||||
private object DataObject;
|
||||
|
||||
public PropBinder(PropertyInfo propertyInfo, object dataObject)
|
||||
{
|
||||
PropertyInfo = propertyInfo;
|
||||
DataObject = dataObject;
|
||||
}
|
||||
|
||||
public string StringValue
|
||||
{
|
||||
get => (string)PropertyInfo.GetValue(DataObject)!;
|
||||
set => PropertyInfo.SetValue(DataObject, value);
|
||||
}
|
||||
|
||||
public int IntValue
|
||||
{
|
||||
get => (int)PropertyInfo.GetValue(DataObject)!;
|
||||
set => PropertyInfo.SetValue(DataObject, value);
|
||||
}
|
||||
|
||||
public long LongValue
|
||||
{
|
||||
get => (long)PropertyInfo.GetValue(DataObject)!;
|
||||
set => PropertyInfo.SetValue(DataObject, value);
|
||||
}
|
||||
|
||||
public bool BoolValue
|
||||
{
|
||||
get => (bool)PropertyInfo.GetValue(DataObject)!;
|
||||
set => PropertyInfo.SetValue(DataObject, value);
|
||||
}
|
||||
|
||||
public DateTime DateTimeValue
|
||||
{
|
||||
get => (DateTime)PropertyInfo.GetValue(DataObject)!;
|
||||
set => PropertyInfo.SetValue(DataObject, value);
|
||||
}
|
||||
|
||||
public T Class
|
||||
{
|
||||
get => (T)PropertyInfo.GetValue(DataObject)!;
|
||||
set => PropertyInfo.SetValue(DataObject, value);
|
||||
}
|
||||
|
||||
public double DoubleValue
|
||||
{
|
||||
get => (double)PropertyInfo.GetValue(DataObject)!;
|
||||
set => PropertyInfo.SetValue(DataObject, value);
|
||||
}
|
||||
}
|
8
Moonlight/App/Models/Abstractions/PaymentGateway.cs
Normal file
8
Moonlight/App/Models/Abstractions/PaymentGateway.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Moonlight.App.Models.Abstractions;
|
||||
|
||||
public abstract class PaymentGateway
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public abstract string Icon { get; }
|
||||
public abstract Task<string> Start(double price);
|
||||
}
|
10
Moonlight/App/Models/Abstractions/ServiceActions.cs
Normal file
10
Moonlight/App/Models/Abstractions/ServiceActions.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
using Moonlight.App.Database.Entities.Store;
|
||||
|
||||
namespace Moonlight.App.Models.Abstractions;
|
||||
|
||||
public abstract class ServiceActions
|
||||
{
|
||||
public abstract Task Create(IServiceProvider provider, Service service);
|
||||
public abstract Task Update(IServiceProvider provider, Service service);
|
||||
public abstract Task Delete(IServiceProvider provider, Service service);
|
||||
}
|
|
@ -9,6 +9,7 @@ public enum Permission
|
|||
AdminSessions = 1002,
|
||||
AdminUsersEdit = 1003,
|
||||
AdminTickets = 1004,
|
||||
AdminStore = 1900,
|
||||
AdminViewExceptions = 1999,
|
||||
AdminRoot = 2000
|
||||
}
|
16
Moonlight/App/Models/Forms/Admin/Store/AddCouponForm.cs
Normal file
16
Moonlight/App/Models/Forms/Admin/Store/AddCouponForm.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms.Admin.Store;
|
||||
|
||||
public class AddCouponForm
|
||||
{
|
||||
[MinLength(5, ErrorMessage = "The code needs to be longer than 4")]
|
||||
[MaxLength(15, ErrorMessage = "The code should not be longer than 15 characters")]
|
||||
public string Code { get; set; } = "";
|
||||
|
||||
[Range(1, 99, ErrorMessage = "The percent needs to be between 1 and 99")]
|
||||
public int Percent { get; set; }
|
||||
|
||||
[Range(0, int.MaxValue, ErrorMessage = "The amount needs to be equals or greater than 0")]
|
||||
public int Amount { get; set; }
|
||||
}
|
16
Moonlight/App/Models/Forms/Admin/Store/AddGiftCodeForm.cs
Normal file
16
Moonlight/App/Models/Forms/Admin/Store/AddGiftCodeForm.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms.Admin.Store;
|
||||
|
||||
public class AddGiftCodeForm
|
||||
{
|
||||
[MinLength(5, ErrorMessage = "The code needs to be longer than 4")]
|
||||
[MaxLength(15, ErrorMessage = "The code should not be longer than 15 characters")]
|
||||
public string Code { get; set; } = "";
|
||||
|
||||
[Range(0, int.MaxValue, ErrorMessage = "The value needs to be equals or greater than 0")]
|
||||
public double Value { get; set; }
|
||||
|
||||
[Range(0, int.MaxValue, ErrorMessage = "The amount needs to be equals or greater than 0")]
|
||||
public int Amount { get; set; }
|
||||
}
|
16
Moonlight/App/Models/Forms/Admin/Store/EditCouponForm.cs
Normal file
16
Moonlight/App/Models/Forms/Admin/Store/EditCouponForm.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms.Admin.Store;
|
||||
|
||||
public class EditCouponForm
|
||||
{
|
||||
[MinLength(5, ErrorMessage = "The code needs to be longer than 4")]
|
||||
[MaxLength(15, ErrorMessage = "The code should not be longer than 15 characters")]
|
||||
public string Code { get; set; } = "";
|
||||
|
||||
[Range(1, 99, ErrorMessage = "The percent needs to be between 1 and 99")]
|
||||
public int Percent { get; set; }
|
||||
|
||||
[Range(0, int.MaxValue, ErrorMessage = "The amount needs to be equals or greater than 0")]
|
||||
public int Amount { get; set; }
|
||||
}
|
16
Moonlight/App/Models/Forms/Admin/Store/EditGiftCodeForm.cs
Normal file
16
Moonlight/App/Models/Forms/Admin/Store/EditGiftCodeForm.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms.Admin.Store;
|
||||
|
||||
public class EditGiftCodeForm
|
||||
{
|
||||
[MinLength(5, ErrorMessage = "The code needs to be longer than 4")]
|
||||
[MaxLength(15, ErrorMessage = "The code should not be longer than 15 characters")]
|
||||
public string Code { get; set; } = "";
|
||||
|
||||
[Range(0, int.MaxValue, ErrorMessage = "The value needs to be equals or greater than 0")]
|
||||
public double Value { get; set; }
|
||||
|
||||
[Range(0, int.MaxValue, ErrorMessage = "The amount needs to be equals or greater than 0")]
|
||||
public int Amount { get; set; }
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
namespace Moonlight.App.Models.Forms.Admin.Users;
|
||||
|
||||
public class UpdateUserForm
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
namespace Moonlight.App.Models.Forms.Admin.Users;
|
||||
|
||||
public class UpdateUserPasswordForm
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
namespace Moonlight.App.Models.Forms.Auth;
|
||||
|
||||
public class LoginForm
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
namespace Moonlight.App.Models.Forms.Auth;
|
||||
|
||||
public class RegisterForm
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
namespace Moonlight.App.Models.Forms.Auth;
|
||||
|
||||
public class ResetPasswordForm
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
namespace Moonlight.App.Models.Forms.Auth;
|
||||
|
||||
public class TwoFactorCodeForm
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
namespace Moonlight.App.Models.Forms.Auth;
|
||||
|
||||
public class UpdateAccountForm
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms;
|
||||
namespace Moonlight.App.Models.Forms.Auth;
|
||||
|
||||
public class UpdateAccountPasswordForm
|
||||
{
|
13
Moonlight/App/Models/Forms/Services/AddUserToServiceForm.cs
Normal file
13
Moonlight/App/Models/Forms/Services/AddUserToServiceForm.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms.Services;
|
||||
|
||||
public class AddUserToServiceForm
|
||||
{
|
||||
[Required(ErrorMessage = "You need to provide an username")]
|
||||
[MinLength(7, ErrorMessage = "The username is too short")]
|
||||
[MaxLength(20, ErrorMessage = "The username cannot be longer than 20 characters")]
|
||||
[RegularExpression("^[a-z][a-z0-9]*$",
|
||||
ErrorMessage = "Usernames can only contain lowercase characters and numbers")]
|
||||
public string Username { get; set; } = "";
|
||||
}
|
15
Moonlight/App/Models/Forms/Store/AddCategoryForm.cs
Normal file
15
Moonlight/App/Models/Forms/Store/AddCategoryForm.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms.Store;
|
||||
|
||||
public class AddCategoryForm
|
||||
{
|
||||
[Required(ErrorMessage = "You need to specify a name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a slug")]
|
||||
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
||||
public string Slug { get; set; } = "";
|
||||
}
|
40
Moonlight/App/Models/Forms/Store/AddProductForm.cs
Normal file
40
Moonlight/App/Models/Forms/Store/AddProductForm.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Database.Enums;
|
||||
using Moonlight.App.Extensions.Attributes;
|
||||
|
||||
namespace Moonlight.App.Models.Forms.Store;
|
||||
|
||||
public class AddProductForm
|
||||
{
|
||||
[Required(ErrorMessage = "You need to specify a category")]
|
||||
[Selector(DisplayProp = "Name", SelectorProp = "Name", UseDropdown = true)]
|
||||
public Category Category { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a name")]
|
||||
[Description("Teeeeeeeeeeeeeeeeeeeeeeeeeest")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a description")]
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a slug")]
|
||||
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
||||
public string Slug { get; set; } = "";
|
||||
|
||||
[Range(0, double.MaxValue, ErrorMessage = "The price needs to be equals or greater than 0")]
|
||||
public double Price { get; set; }
|
||||
|
||||
[Range(0, double.MaxValue, ErrorMessage = "The stock needs to be equals or greater than 0")]
|
||||
public int Stock { get; set; }
|
||||
|
||||
[Range(0, double.MaxValue, ErrorMessage = "The max per user amount needs to be equals or greater than 0")]
|
||||
public int MaxPerUser { get; set; }
|
||||
|
||||
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or greater than 0")]
|
||||
public int Duration { get; set; }
|
||||
|
||||
public ServiceType Type { get; set; }
|
||||
public string ConfigJson { get; set; } = "{}";
|
||||
}
|
15
Moonlight/App/Models/Forms/Store/EditCategoryForm.cs
Normal file
15
Moonlight/App/Models/Forms/Store/EditCategoryForm.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.App.Models.Forms.Store;
|
||||
|
||||
public class EditCategoryForm
|
||||
{
|
||||
[Required(ErrorMessage = "You need to specify a name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a slug")]
|
||||
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
||||
public string Slug { get; set; } = "";
|
||||
}
|
36
Moonlight/App/Models/Forms/Store/EditProductForm.cs
Normal file
36
Moonlight/App/Models/Forms/Store/EditProductForm.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Database.Enums;
|
||||
|
||||
namespace Moonlight.App.Models.Forms.Store;
|
||||
|
||||
public class EditProductForm
|
||||
{
|
||||
[Required(ErrorMessage = "You need to specify a category")]
|
||||
public Category Category { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a description")]
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a slug")]
|
||||
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
||||
public string Slug { get; set; } = "";
|
||||
|
||||
[Range(0, double.MaxValue, ErrorMessage = "The price needs to be equals or above 0")]
|
||||
public double Price { get; set; }
|
||||
|
||||
[Range(0, double.MaxValue, ErrorMessage = "The stock needs to be equals or above 0")]
|
||||
public int Stock { get; set; }
|
||||
|
||||
[Range(0, double.MaxValue, ErrorMessage = "The max per user amount needs to be equals or above 0")]
|
||||
public int MaxPerUser { get; set; }
|
||||
|
||||
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")]
|
||||
public int Duration { get; set; }
|
||||
|
||||
public ServiceType Type { get; set; }
|
||||
public string ConfigJson { get; set; } = "{}";
|
||||
}
|
12
Moonlight/App/Plugins/Contexts/PluginContext.cs
Normal file
12
Moonlight/App/Plugins/Contexts/PluginContext.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace Moonlight.App.Plugins.Contexts;
|
||||
|
||||
public class PluginContext
|
||||
{
|
||||
public IServiceCollection Services { get; set; }
|
||||
public IServiceProvider Provider { get; set; }
|
||||
public IServiceScope Scope { get; set; }
|
||||
public WebApplicationBuilder WebApplicationBuilder { get; set; }
|
||||
public WebApplication WebApplication { get; set; }
|
||||
public List<Action> PreInitTasks = new();
|
||||
public List<Action> PostInitTasks = new();
|
||||
}
|
10
Moonlight/App/Plugins/MoonlightPlugin.cs
Normal file
10
Moonlight/App/Plugins/MoonlightPlugin.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
using Moonlight.App.Plugins.Contexts;
|
||||
|
||||
namespace Moonlight.App.Plugins;
|
||||
|
||||
public abstract class MoonlightPlugin
|
||||
{
|
||||
public PluginContext Context { get; set; }
|
||||
public abstract Task Enable();
|
||||
public abstract Task Disable();
|
||||
}
|
|
@ -1,23 +1,53 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Event;
|
||||
using Moonlight.App.Event.Args;
|
||||
|
||||
namespace Moonlight.App.Services.Background;
|
||||
|
||||
public class AutoMailSendService // This service is responsible for sending mails automatically
|
||||
{
|
||||
private readonly MailService MailService;
|
||||
private readonly ConfigService ConfigService;
|
||||
|
||||
public AutoMailSendService(MailService mailService, ConfigService configService)
|
||||
public AutoMailSendService(MailService mailService)
|
||||
{
|
||||
MailService = mailService;
|
||||
ConfigService = configService;
|
||||
|
||||
|
||||
Events.OnUserRegistered += OnUserRegistered;
|
||||
Events.OnServiceOrdered += OnServiceOrdered;
|
||||
Events.OnTransactionCreated += OnTransactionCreated;
|
||||
}
|
||||
|
||||
private async void OnTransactionCreated(object? sender, TransactionCreatedEventArgs eventArgs)
|
||||
{
|
||||
await MailService.Send(
|
||||
eventArgs.User,
|
||||
"New transaction",
|
||||
"transactionCreated",
|
||||
eventArgs.Transaction,
|
||||
eventArgs.User
|
||||
);
|
||||
}
|
||||
|
||||
private async void OnServiceOrdered(object? _, Service service)
|
||||
{
|
||||
await MailService.Send(
|
||||
service.Owner,
|
||||
"New product ordered",
|
||||
"serviceOrdered",
|
||||
service,
|
||||
service.Product,
|
||||
service.Owner
|
||||
);
|
||||
}
|
||||
|
||||
private async void OnUserRegistered(object? _, User user)
|
||||
{
|
||||
await MailService.Send(user, $"Welcome {user.Username}", "welcome", user);
|
||||
await MailService.Send(
|
||||
user,
|
||||
$"Welcome {user.Username}",
|
||||
"welcome",
|
||||
user
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Helpers;
|
||||
using Moonlight.App.Models.Abstractions;
|
||||
|
@ -23,6 +25,7 @@ public class IdentityService
|
|||
public bool IsSignedIn => CurrentUserNullable != null;
|
||||
public FlagStorage Flags { get; private set; } = new("");
|
||||
public PermissionStorage Permissions { get; private set; } = new(-1);
|
||||
public Transaction[] Transactions => GetTransactions().Result; // TODO: make more efficient
|
||||
public EventHandler OnAuthenticationStateChanged { get; set; }
|
||||
|
||||
public IdentityService(Repository<User> userRepository,
|
||||
|
@ -31,6 +34,20 @@ public class IdentityService
|
|||
UserRepository = userRepository;
|
||||
JwtService = jwtService;
|
||||
}
|
||||
|
||||
// Transactions
|
||||
public Task<Transaction[]> GetTransactions()
|
||||
{
|
||||
if (CurrentUserNullable == null)
|
||||
return Task.FromResult(Array.Empty<Transaction>());
|
||||
|
||||
var user = UserRepository
|
||||
.Get()
|
||||
.Include(x => x.Transactions)
|
||||
.First(x => x.Id == CurrentUserNullable.Id);
|
||||
|
||||
return Task.FromResult(user.Transactions.ToArray());
|
||||
}
|
||||
|
||||
// Authentication
|
||||
|
||||
|
|
50
Moonlight/App/Services/Interop/AlertService.cs
Normal file
50
Moonlight/App/Services/Interop/AlertService.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Moonlight.App.Services.Interop;
|
||||
|
||||
public class AlertService
|
||||
{
|
||||
private readonly IJSRuntime JsRuntime;
|
||||
|
||||
public AlertService(IJSRuntime jsRuntime)
|
||||
{
|
||||
JsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task Info(string title, string message)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.alerts.info", title, message);
|
||||
}
|
||||
|
||||
public async Task Success(string title, string message)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.alerts.success", title, message);
|
||||
}
|
||||
|
||||
public async Task Warning(string title, string message)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.alerts.warning", title, message);
|
||||
}
|
||||
|
||||
public async Task Error(string title, string message)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.alerts.error", title, message);
|
||||
}
|
||||
|
||||
public async Task<string> Text(string title, string message)
|
||||
{
|
||||
return await JsRuntime.InvokeAsync<string>("moonlight.alerts.text", title, message);
|
||||
}
|
||||
|
||||
public async Task<bool> YesNo(string title, string yes, string no)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await JsRuntime.InvokeAsync<bool>("moonlight.alerts.yesno", title, yes, no);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
37
Moonlight/App/Services/Interop/ModalService.cs
Normal file
37
Moonlight/App/Services/Interop/ModalService.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Moonlight.App.Services.Interop;
|
||||
|
||||
public class ModalService
|
||||
{
|
||||
private readonly IJSRuntime JsRuntime;
|
||||
|
||||
public ModalService(IJSRuntime jsRuntime)
|
||||
{
|
||||
JsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task Show(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.modals.show", id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Hide(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.modals.hide", id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
122
Moonlight/App/Services/PluginService.cs
Normal file
122
Moonlight/App/Services/PluginService.cs
Normal file
|
@ -0,0 +1,122 @@
|
|||
using System.Reflection;
|
||||
using Moonlight.App.Helpers;
|
||||
using Moonlight.App.Plugins;
|
||||
using Moonlight.App.Plugins.Contexts;
|
||||
|
||||
namespace Moonlight.App.Services;
|
||||
|
||||
public class PluginService
|
||||
{
|
||||
private readonly List<MoonlightPlugin> Plugins = new();
|
||||
|
||||
public async Task Load(WebApplicationBuilder webApplicationBuilder)
|
||||
{
|
||||
var path = PathBuilder.Dir("storage", "plugins");
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
var files = FindFiles(path)
|
||||
.Where(x => x.EndsWith(".dll"))
|
||||
.ToArray();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFile(PathBuilder.File(Directory.GetCurrentDirectory(), file));
|
||||
|
||||
int plugins = 0;
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
if (type.IsSubclassOf(typeof(MoonlightPlugin)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var plugin = (Activator.CreateInstance(type) as MoonlightPlugin)!;
|
||||
|
||||
// Create environment
|
||||
plugin.Context = new PluginContext()
|
||||
{
|
||||
Services = webApplicationBuilder.Services,
|
||||
WebApplicationBuilder = webApplicationBuilder
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await plugin.Enable();
|
||||
|
||||
// After here we can treat the plugin as successfully loaded
|
||||
plugins++;
|
||||
Plugins.Add(plugin);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Fatal($"Unhandled exception while enabling plugin '{type.Name}'");
|
||||
Logger.Fatal(e);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Fatal($"Failed to create plugin environment for '{type.Name}'");
|
||||
Logger.Fatal(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(plugins == 0) // If 0, we can assume that it was a library dll
|
||||
Logger.Info($"Loaded {file} as a library");
|
||||
else
|
||||
Logger.Info($"Loaded {plugins} plugin(s) from {file}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Fatal($"Unable to load assembly from file '{file}'");
|
||||
Logger.Fatal(e);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info($"Loaded {Plugins.Count} plugin(s)");
|
||||
}
|
||||
|
||||
public async Task RunPreInit()
|
||||
{
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
Logger.Info($"Running pre init tasks for {plugin.GetType().Name}");
|
||||
|
||||
foreach (var preInitTask in plugin.Context.PreInitTasks)
|
||||
await Task.Run(preInitTask);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RunPrePost(WebApplication webApplication)
|
||||
{
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
// Pass through the dependency injection
|
||||
var scope = webApplication.Services.CreateScope();
|
||||
plugin.Context.Provider = scope.ServiceProvider;
|
||||
plugin.Context.Scope = scope;
|
||||
plugin.Context.WebApplication = webApplication;
|
||||
|
||||
Logger.Info($"Running post init tasks for {plugin.GetType().Name}");
|
||||
|
||||
foreach (var postInitTask in plugin.Context.PostInitTasks)
|
||||
await Task.Run(postInitTask);
|
||||
}
|
||||
}
|
||||
|
||||
private string[] FindFiles(string dir)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
foreach (var file in Directory.GetFiles(dir))
|
||||
result.Add(file);
|
||||
|
||||
foreach (var directory in Directory.GetDirectories(dir))
|
||||
{
|
||||
result.AddRange(FindFiles(directory));
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
91
Moonlight/App/Services/ServiceManage/ServiceAdminService.cs
Normal file
91
Moonlight/App/Services/ServiceManage/ServiceAdminService.cs
Normal file
|
@ -0,0 +1,91 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Database.Enums;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Models.Abstractions;
|
||||
using Moonlight.App.Repositories;
|
||||
|
||||
namespace Moonlight.App.Services.ServiceManage;
|
||||
|
||||
public class ServiceAdminService
|
||||
{
|
||||
public readonly Dictionary<ServiceType, ServiceActions> Actions = new();
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
|
||||
public ServiceAdminService(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public async Task<Service> Create(User u, Product p, Action<Service>? modifyService = null)
|
||||
{
|
||||
if (!Actions.ContainsKey(p.Type))
|
||||
throw new DisplayException($"The product type {p.Type} is not registered");
|
||||
|
||||
// Load models in new scope
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
var productRepo = scope.ServiceProvider.GetRequiredService<Repository<Product>>();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
|
||||
var user = userRepo.Get().First(x => x.Id == u.Id);
|
||||
var product = productRepo.Get().First(x => x.Id == p.Id);
|
||||
|
||||
// Create database model
|
||||
var service = new Service()
|
||||
{
|
||||
Product = product,
|
||||
Owner = user,
|
||||
Suspended = false,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Allow further modifications
|
||||
if(modifyService != null)
|
||||
modifyService.Invoke(service);
|
||||
|
||||
// Add new service in database
|
||||
var finishedService = serviceRepo.Add(service);
|
||||
|
||||
// Call the action for the logic behind the service type
|
||||
var actions = Actions[product.Type];
|
||||
await actions.Create(scope.ServiceProvider, finishedService);
|
||||
|
||||
return finishedService;
|
||||
}
|
||||
|
||||
public async Task Delete(Service s)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
var serviceShareRepo = scope.ServiceProvider.GetRequiredService<Repository<ServiceShare>>();
|
||||
|
||||
var service = serviceRepo
|
||||
.Get()
|
||||
.Include(x => x.Product)
|
||||
.Include(x => x.Shares)
|
||||
.FirstOrDefault(x => x.Id == s.Id);
|
||||
|
||||
if (service == null)
|
||||
throw new DisplayException("Service does not exist anymore");
|
||||
|
||||
if (!Actions.ContainsKey(service.Product.Type))
|
||||
throw new DisplayException($"The product type {service.Product.Type} is not registered");
|
||||
|
||||
await Actions[service.Product.Type].Delete(scope.ServiceProvider, service);
|
||||
|
||||
foreach (var share in service.Shares)
|
||||
{
|
||||
serviceShareRepo.Delete(share);
|
||||
}
|
||||
|
||||
serviceRepo.Delete(service);
|
||||
}
|
||||
|
||||
public Task RegisterAction(ServiceType type, ServiceActions actions) // Use this function to register service types
|
||||
{
|
||||
Actions.Add(type, actions);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
112
Moonlight/App/Services/ServiceManage/ServiceService.cs
Normal file
112
Moonlight/App/Services/ServiceManage/ServiceService.cs
Normal file
|
@ -0,0 +1,112 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Repositories;
|
||||
|
||||
namespace Moonlight.App.Services.ServiceManage;
|
||||
|
||||
public class ServiceService // This service is used for managing services and create the connection to the actual logic behind a service type
|
||||
{
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
private readonly Repository<Service> ServiceRepository;
|
||||
private readonly Repository<User> UserRepository;
|
||||
|
||||
public ServiceAdminService Admin => ServiceProvider.GetRequiredService<ServiceAdminService>();
|
||||
|
||||
public ServiceService(IServiceProvider serviceProvider, Repository<Service> serviceRepository, Repository<User> userRepository)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
ServiceRepository = serviceRepository;
|
||||
UserRepository = userRepository;
|
||||
}
|
||||
|
||||
public Task<Service[]> Get(User user)
|
||||
{
|
||||
var result = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Product)
|
||||
.Where(x => x.Owner.Id == user.Id)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<Service[]> GetShared(User user)
|
||||
{
|
||||
var result = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Product)
|
||||
.Include(x => x.Owner)
|
||||
.Where(x => x.Shares.Any(y => y.User.Id == user.Id))
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task AddSharedUser(Service s, string username)
|
||||
{
|
||||
var userToAdd = UserRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Username == username);
|
||||
|
||||
if (userToAdd == null)
|
||||
throw new DisplayException("No user found with this username");
|
||||
|
||||
var service = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Include(x => x.Shares)
|
||||
.ThenInclude(x => x.User)
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
if (service.Owner.Id == userToAdd.Id)
|
||||
throw new DisplayException("The owner cannot be added as a shared user");
|
||||
|
||||
if (service.Shares.Any(x => x.User.Id == userToAdd.Id))
|
||||
throw new DisplayException("The user has already access to this service");
|
||||
|
||||
service.Shares.Add(new ()
|
||||
{
|
||||
User = userToAdd
|
||||
});
|
||||
|
||||
ServiceRepository.Update(service);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<User[]> GetSharedUsers(Service s)
|
||||
{
|
||||
var service = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Shares)
|
||||
.ThenInclude(x => x.User)
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
var result = service.Shares
|
||||
.Select(x => x.User)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task RemoveSharedUser(Service s, User user)
|
||||
{
|
||||
var service = ServiceRepository
|
||||
.Get()
|
||||
.Include(x => x.Shares)
|
||||
.ThenInclude(x => x.User)
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
var shareToRemove = service.Shares.FirstOrDefault(x => x.User.Id == user.Id);
|
||||
|
||||
if (shareToRemove == null)
|
||||
throw new DisplayException("This user does not have access to this service");
|
||||
|
||||
service.Shares.Remove(shareToRemove);
|
||||
ServiceRepository.Update(service);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
99
Moonlight/App/Services/Store/StoreAdminService.cs
Normal file
99
Moonlight/App/Services/Store/StoreAdminService.cs
Normal file
|
@ -0,0 +1,99 @@
|
|||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Database.Enums;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Repositories;
|
||||
|
||||
namespace Moonlight.App.Services.Store;
|
||||
|
||||
public class StoreAdminService
|
||||
{
|
||||
private readonly Repository<Product> ProductRepository;
|
||||
private readonly Repository<Category> CategoryRepository;
|
||||
|
||||
public StoreAdminService(Repository<Product> productRepository, Repository<Category> categoryRepository)
|
||||
{
|
||||
ProductRepository = productRepository;
|
||||
CategoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
public Task<Category> AddCategory(string name, string description, string slug)
|
||||
{
|
||||
if (CategoryRepository.Get().Any(x => x.Slug == slug))
|
||||
throw new DisplayException("A category with this slug does already exist");
|
||||
|
||||
var result = CategoryRepository.Add(new Category()
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Slug = slug
|
||||
});
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<Product> AddProduct(string name, string description, string slug, ServiceType type, string configJson,
|
||||
Action<Product>? modifyProduct = null)
|
||||
{
|
||||
if (ProductRepository.Get().Any(x => x.Slug == slug))
|
||||
throw new DisplayException("A product with that slug does already exist");
|
||||
|
||||
var product = new Product()
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Slug = slug,
|
||||
Type = type,
|
||||
ConfigJson = configJson
|
||||
};
|
||||
|
||||
if(modifyProduct != null)
|
||||
modifyProduct.Invoke(product);
|
||||
|
||||
var result = ProductRepository.Add(product);
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task UpdateCategory(Category category)
|
||||
{
|
||||
if (CategoryRepository.Get().Any(x => x.Id != category.Id && x.Slug == category.Slug))
|
||||
throw new DisplayException("A category with that slug does already exist");
|
||||
|
||||
CategoryRepository.Update(category);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UpdateProduct(Product product)
|
||||
{
|
||||
if (ProductRepository.Get().Any(x => x.Id != product.Id && x.Slug == product.Slug))
|
||||
throw new DisplayException("A product with that slug does already exist");
|
||||
|
||||
ProductRepository.Update(product);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteCategory(Category category)
|
||||
{
|
||||
var hasProductsInIt = ProductRepository
|
||||
.Get()
|
||||
.Any(x => x.Category!.Id == category.Id);
|
||||
|
||||
if (hasProductsInIt)
|
||||
throw new DisplayException("The category contains products. You need to delete the products in order to delete the category");
|
||||
|
||||
CategoryRepository.Delete(category);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteProduct(Product product)
|
||||
{
|
||||
//TODO: Implement checks if services with that product id exist
|
||||
|
||||
ProductRepository.Delete(product);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
60
Moonlight/App/Services/Store/StoreGiftService.cs
Normal file
60
Moonlight/App/Services/Store/StoreGiftService.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Repositories;
|
||||
|
||||
namespace Moonlight.App.Services.Store;
|
||||
|
||||
public class StoreGiftService
|
||||
{
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
|
||||
public StoreGiftService(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public async Task Redeem(User u, string code)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var giftCodeRepo = scope.ServiceProvider.GetRequiredService<Repository<GiftCode>>();
|
||||
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
|
||||
var user = userRepo
|
||||
.Get()
|
||||
.Include(x => x.GiftCodeUses)
|
||||
.ThenInclude(x => x.GiftCode)
|
||||
.FirstOrDefault(x => x.Id == u.Id);
|
||||
|
||||
if(user == null)
|
||||
throw new DisplayException("Unsafe value detected. Please reload the page to proceed");
|
||||
|
||||
var giftCode = giftCodeRepo
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Code == code);
|
||||
|
||||
if (giftCode == null)
|
||||
throw new DisplayException("The gift code does not exist");
|
||||
|
||||
if (giftCode.Amount < 1)
|
||||
throw new DisplayException("The gift code can no longer be used as it has exceeded the max use amount");
|
||||
|
||||
if (user.GiftCodeUses.Any(x => x.GiftCode.Id == giftCode.Id))
|
||||
throw new DisplayException("The gift code has already been used on this account");
|
||||
|
||||
giftCode.Amount--;
|
||||
giftCodeRepo.Update(giftCode);
|
||||
|
||||
var giftCardUse = new GiftCodeUse()
|
||||
{
|
||||
GiftCode = giftCode
|
||||
};
|
||||
|
||||
user.GiftCodeUses.Add(giftCardUse);
|
||||
userRepo.Update(user);
|
||||
|
||||
var transactionService = scope.ServiceProvider.GetRequiredService<TransactionService>();
|
||||
await transactionService.Add(u, giftCode.Value, $"Redeemed gift code '{giftCode.Code}'");
|
||||
}
|
||||
}
|
204
Moonlight/App/Services/Store/StoreOrderService.cs
Normal file
204
Moonlight/App/Services/Store/StoreOrderService.cs
Normal file
|
@ -0,0 +1,204 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Event;
|
||||
using Moonlight.App.Exceptions;
|
||||
using Moonlight.App.Extensions;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services.ServiceManage;
|
||||
|
||||
namespace Moonlight.App.Services.Store;
|
||||
|
||||
public class StoreOrderService
|
||||
{
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
|
||||
public StoreOrderService(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public Task Validate(User u, Product p, int durationMultiplier, Coupon? c)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var productRepo = scope.ServiceProvider.GetRequiredService<Repository<Product>>();
|
||||
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
var couponRepo = scope.ServiceProvider.GetRequiredService<Repository<Coupon>>();
|
||||
|
||||
// Ensure the values are safe and loaded by using the created scope to bypass the cache
|
||||
|
||||
var user = userRepo
|
||||
.Get()
|
||||
.Include(x => x.CouponUses)
|
||||
.ThenInclude(x => x.Coupon)
|
||||
.FirstOrDefault(x => x.Id == u.Id);
|
||||
|
||||
if (user == null)
|
||||
throw new DisplayException("Unsafe value detected. Please reload the page to proceed");
|
||||
|
||||
|
||||
var product = productRepo
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Id == p.Id);
|
||||
|
||||
if (product == null)
|
||||
throw new DisplayException("Unsafe value detected. Please reload the page to proceed");
|
||||
|
||||
|
||||
Coupon? coupon = c;
|
||||
|
||||
if (coupon != null) // Only check if the coupon actually has a value
|
||||
{
|
||||
coupon = couponRepo
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Id == coupon.Id);
|
||||
|
||||
if (coupon == null)
|
||||
throw new DisplayException("Unsafe value detected. Please reload the page to proceed");
|
||||
}
|
||||
|
||||
// Perform checks on selected order
|
||||
|
||||
if (coupon != null && user.CouponUses.Any(x => x.Coupon.Id == coupon.Id))
|
||||
throw new DisplayException("Coupon already used");
|
||||
|
||||
if (coupon != null && coupon.Amount < 1)
|
||||
throw new DisplayException("No coupon uses left");
|
||||
|
||||
var price = product.Price * durationMultiplier;
|
||||
|
||||
if (coupon != null)
|
||||
price = Math.Round(price - (price * coupon.Percent / 100), 2);
|
||||
|
||||
if (user.Balance < price)
|
||||
throw new DisplayException("Order is too expensive");
|
||||
|
||||
var userServices = serviceRepo
|
||||
.Get()
|
||||
.Where(x => x.Product.Id == product.Id)
|
||||
.Count(x => x.Owner.Id == user.Id);
|
||||
|
||||
if (userServices >= product.MaxPerUser)
|
||||
throw new DisplayException("The limit of this product on your account has been reached");
|
||||
|
||||
if (product.Stock < 1)
|
||||
throw new DisplayException("The product is out of stock");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<Service> Process(User u, Product p, int durationMultiplier, Coupon? c)
|
||||
{
|
||||
// Validate to ensure we dont process an illegal order
|
||||
await Validate(u, p, durationMultiplier, c);
|
||||
|
||||
// Create scope and get required services
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var serviceService = scope.ServiceProvider.GetRequiredService<ServiceService>();
|
||||
var transactionService = scope.ServiceProvider.GetRequiredService<TransactionService>();
|
||||
|
||||
// Calculate price
|
||||
var price = p.Price * durationMultiplier;
|
||||
|
||||
if (c != null)
|
||||
price = Math.Round(price - (price * c.Percent / 100), 2);
|
||||
|
||||
// Calculate duration
|
||||
var duration = durationMultiplier * p.Duration;
|
||||
|
||||
// Add transaction
|
||||
await transactionService.Add(u, -1 * price, $"Bought product '{p.Name}' for {duration} days");
|
||||
|
||||
// Process coupon if used
|
||||
if (c != null)
|
||||
{
|
||||
// Remove one use from the coupon
|
||||
var couponRepo = scope.ServiceProvider.GetRequiredService<Repository<Coupon>>();
|
||||
|
||||
var coupon = couponRepo
|
||||
.Get()
|
||||
.First(x => x.Id == c.Id);
|
||||
|
||||
coupon.Amount--;
|
||||
couponRepo.Update(coupon);
|
||||
|
||||
// Add coupon use to user
|
||||
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
|
||||
var user = userRepo
|
||||
.Get()
|
||||
.Include(x => x.CouponUses)
|
||||
.First(x => x.Id == u.Id);
|
||||
|
||||
user.CouponUses.Add(new ()
|
||||
{
|
||||
Coupon = coupon
|
||||
});
|
||||
|
||||
userRepo.Update(user);
|
||||
}
|
||||
|
||||
// Create service
|
||||
var service = await serviceService.Admin.Create(u, p,
|
||||
service => { service.RenewAt = DateTime.UtcNow.AddDays(duration); });
|
||||
|
||||
await Events.OnServiceOrdered.InvokeAsync(service);
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
public Task ValidateRenew(User u, Service s, int durationMultiplier)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
|
||||
var user = userRepo.Get().FirstOrDefault(x => x.Id == u.Id);
|
||||
|
||||
if(user == null)
|
||||
throw new DisplayException("Unsafe value detected. Please reload the page to proceed");
|
||||
|
||||
var service = serviceRepo
|
||||
.Get()
|
||||
.Include(x => x.Product)
|
||||
.Include(x => x.Owner)
|
||||
.FirstOrDefault(x => x.Id == s.Id);
|
||||
|
||||
if(service == null)
|
||||
throw new DisplayException("Unsafe value detected. Please reload the page to proceed");
|
||||
|
||||
var price = service.Product.Price * durationMultiplier;
|
||||
|
||||
if (user.Balance < price)
|
||||
throw new DisplayException("Order is too expensive");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Renew(User u, Service s, int durationMultiplier)
|
||||
{
|
||||
await ValidateRenew(u, s, durationMultiplier);
|
||||
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
|
||||
var transactionService = scope.ServiceProvider.GetRequiredService<TransactionService>();
|
||||
|
||||
var service = serviceRepo
|
||||
.Get()
|
||||
.Include(x => x.Product)
|
||||
.Include(x => x.Owner)
|
||||
.First(x => x.Id == s.Id);
|
||||
|
||||
var price = service.Product.Price * durationMultiplier;
|
||||
|
||||
// Calculate duration
|
||||
var duration = durationMultiplier * service.Product.Duration;
|
||||
|
||||
// Add transaction
|
||||
await transactionService.Add(u, -1 * price, $"Renewed service '{service.Nickname ?? $"Service {service.Id}"}' for {duration} days");
|
||||
|
||||
service.RenewAt = service.RenewAt.AddDays(duration);
|
||||
serviceRepo.Update(service);
|
||||
}
|
||||
}
|
14
Moonlight/App/Services/Store/StorePaymentService.cs
Normal file
14
Moonlight/App/Services/Store/StorePaymentService.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using Moonlight.App.Models.Abstractions;
|
||||
|
||||
namespace Moonlight.App.Services.Store;
|
||||
|
||||
public class StorePaymentService
|
||||
{
|
||||
public readonly List<PaymentGateway> Gateways = new();
|
||||
|
||||
public Task RegisterGateway(PaymentGateway gateway)
|
||||
{
|
||||
Gateways.Add(gateway);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
18
Moonlight/App/Services/Store/StoreService.cs
Normal file
18
Moonlight/App/Services/Store/StoreService.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using Moonlight.App.Models.Abstractions;
|
||||
|
||||
namespace Moonlight.App.Services.Store;
|
||||
|
||||
public class StoreService
|
||||
{
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
|
||||
public StoreAdminService Admin => ServiceProvider.GetRequiredService<StoreAdminService>();
|
||||
public StoreOrderService Order => ServiceProvider.GetRequiredService<StoreOrderService>();
|
||||
public StorePaymentService Payment => ServiceProvider.GetRequiredService<StorePaymentService>();
|
||||
public StoreGiftService Gift => ServiceProvider.GetRequiredService<StoreGiftService>();
|
||||
|
||||
public StoreService(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
}
|
45
Moonlight/App/Services/Store/TransactionService.cs
Normal file
45
Moonlight/App/Services/Store/TransactionService.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Database.Entities.Store;
|
||||
using Moonlight.App.Event;
|
||||
using Moonlight.App.Extensions;
|
||||
using Moonlight.App.Helpers;
|
||||
using Moonlight.App.Repositories;
|
||||
|
||||
namespace Moonlight.App.Services.Store;
|
||||
|
||||
public class TransactionService
|
||||
{
|
||||
private readonly Repository<User> UserRepository;
|
||||
|
||||
public TransactionService(Repository<User> userRepository)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task Add(User u, double amount, string message)
|
||||
{
|
||||
var user = UserRepository.Get().First(x => x.Id == u.Id); // Load user with current repo
|
||||
|
||||
var transaction = new Transaction()
|
||||
{
|
||||
Text = message,
|
||||
Price = amount
|
||||
};
|
||||
|
||||
user.Transactions.Add(transaction);
|
||||
UserRepository.Update(user);
|
||||
|
||||
// We divide the call to ensure the transaction can be written to the database
|
||||
|
||||
user.Balance += amount;
|
||||
user.Balance = Math.Round(user.Balance, 2); // To prevent weird numbers
|
||||
|
||||
UserRepository.Update(user);
|
||||
|
||||
await Events.OnTransactionCreated.InvokeAsync(new()
|
||||
{
|
||||
Transaction = transaction,
|
||||
User = user
|
||||
});
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@
|
|||
<Folder Include="App\Http\Middleware\" />
|
||||
<Folder Include="App\Http\Requests\" />
|
||||
<Folder Include="App\Http\Resources\" />
|
||||
<Folder Include="storage\logs\" />
|
||||
<Folder Include="wwwroot\img\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -26,6 +27,7 @@
|
|||
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
||||
<PackageReference Include="JWT" Version="10.1.1" />
|
||||
<PackageReference Include="MailKit" Version="4.2.0" />
|
||||
<PackageReference Include="Mappy.Net" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
@ -43,4 +45,8 @@
|
|||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0-dev-00923" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="storage\config.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -14,8 +14,10 @@
|
|||
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered"/>
|
||||
|
||||
<link href="/css/theme.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="/css/utils.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="/css/blazor.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="/css/boxicons.min.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="/css/sweetalert2dark.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="https://fonts.googleapis.com/css?family=Inter:300,400,500,600,700" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body data-kt-app-header-fixed="true"
|
||||
|
@ -34,6 +36,7 @@
|
|||
<script src="/js/toaster.js"></script>
|
||||
<script src="/js/moonlight.js"></script>
|
||||
<script src="/_content/BlazorTable/BlazorTable.min.js"></script>
|
||||
<script src="/js/sweetalert2.js"></script>
|
||||
<script src="/_framework/blazor.server.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,5 +1,7 @@
|
|||
using BlazorTable;
|
||||
using Moonlight.App.Actions.Dummy;
|
||||
using Moonlight.App.Database;
|
||||
using Moonlight.App.Database.Enums;
|
||||
using Moonlight.App.Extensions;
|
||||
using Moonlight.App.Helpers;
|
||||
using Moonlight.App.Helpers.LogMigrator;
|
||||
|
@ -7,6 +9,8 @@ using Moonlight.App.Repositories;
|
|||
using Moonlight.App.Services;
|
||||
using Moonlight.App.Services.Background;
|
||||
using Moonlight.App.Services.Interop;
|
||||
using Moonlight.App.Services.ServiceManage;
|
||||
using Moonlight.App.Services.Store;
|
||||
using Moonlight.App.Services.Users;
|
||||
using Moonlight.App.Services.Utils;
|
||||
using Serilog;
|
||||
|
@ -25,6 +29,13 @@ Log.Logger = logConfig.CreateLogger();
|
|||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Init plugin system
|
||||
var pluginService = new PluginService();
|
||||
builder.Services.AddSingleton(pluginService);
|
||||
|
||||
await pluginService.Load(builder);
|
||||
await pluginService.RunPreInit();
|
||||
|
||||
builder.Services.AddDbContext<DataContext>();
|
||||
|
||||
// Repositories
|
||||
|
@ -36,6 +47,16 @@ builder.Services.AddScoped<JwtService>();
|
|||
// Services / Interop
|
||||
builder.Services.AddScoped<CookieService>();
|
||||
builder.Services.AddScoped<ToastService>();
|
||||
builder.Services.AddScoped<ModalService>();
|
||||
builder.Services.AddScoped<AlertService>();
|
||||
|
||||
// Services / Store
|
||||
builder.Services.AddScoped<StoreService>();
|
||||
builder.Services.AddScoped<StoreAdminService>();
|
||||
builder.Services.AddScoped<StoreOrderService>();
|
||||
builder.Services.AddScoped<StoreGiftService>();
|
||||
builder.Services.AddSingleton<StorePaymentService>();
|
||||
builder.Services.AddScoped<TransactionService>();
|
||||
|
||||
// Services / Users
|
||||
builder.Services.AddScoped<UserService>();
|
||||
|
@ -45,6 +66,10 @@ builder.Services.AddScoped<UserDetailsService>();
|
|||
// Services / Background
|
||||
builder.Services.AddSingleton<AutoMailSendService>();
|
||||
|
||||
// Services / ServiceManage
|
||||
builder.Services.AddScoped<ServiceService>();
|
||||
builder.Services.AddSingleton<ServiceAdminService>();
|
||||
|
||||
// Services
|
||||
builder.Services.AddScoped<IdentityService>();
|
||||
builder.Services.AddSingleton<ConfigService>();
|
||||
|
@ -78,4 +103,9 @@ app.MapControllers();
|
|||
// Auto start background services
|
||||
app.Services.GetRequiredService<AutoMailSendService>();
|
||||
|
||||
var serviceService = app.Services.GetRequiredService<ServiceAdminService>();
|
||||
await serviceService.RegisterAction(ServiceType.Server, new DummyActions());
|
||||
|
||||
await pluginService.RunPrePost(app);
|
||||
|
||||
app.Run();
|
|
@ -2,6 +2,7 @@
|
|||
@using Moonlight.App.Services.Users
|
||||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Models.Enums
|
||||
@using Moonlight.App.Models.Forms.Auth
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject UserService UserService
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Models.Forms.Auth
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject CookieService CookieService
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
@using Moonlight.App.Services.Users
|
||||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Models.Forms.Auth
|
||||
|
||||
@inject UserService UserService
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Services.Users
|
||||
@using Moonlight.App.Exceptions
|
||||
@using Moonlight.App.Models.Forms.Auth
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject UserService UserService
|
||||
|
|
235
Moonlight/Shared/Components/Forms/AutoCrud.razor
Normal file
235
Moonlight/Shared/Components/Forms/AutoCrud.razor
Normal file
|
@ -0,0 +1,235 @@
|
|||
@using BlazorTable
|
||||
@using Moonlight.App.Repositories
|
||||
@using System.Linq.Expressions
|
||||
@using Mappy.Net
|
||||
|
||||
@typeparam TItem where TItem : class
|
||||
@typeparam TCreateForm
|
||||
@typeparam TUpdateForm
|
||||
|
||||
@inject Repository<TItem> ItemRepository
|
||||
@inject ToastService ToastService
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">@(Title)</h3>
|
||||
<div class="card-toolbar">
|
||||
<button @onclick="StartCreate" class="btn btn-icon btn-success">
|
||||
<i class="bx bx-sm bx-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<LazyLoader @ref="LazyLoader" Load="LoadItems">
|
||||
<Table TableItem="TItem"
|
||||
Items="Items"
|
||||
PageSize="50"
|
||||
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||
TableHeadClass="fw-bold text-muted">
|
||||
@ChildContent
|
||||
<Column TableItem="TItem" Field="IdExpression" Title="" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<div class="text-end">
|
||||
<div class="btn-group">
|
||||
<button @onclick="() => StartUpdate(context)" class="btn btn-icon btn-warning">
|
||||
<i class="bx bx-sm bx-slider"></i>
|
||||
</button>
|
||||
<button @onclick="() => StartDelete(context)" class="btn btn-icon btn-danger">
|
||||
<i class="bx bx-sm bx-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Template>
|
||||
</Column>
|
||||
</Table>
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SmartModal @ref="CreateModal" CssClasses="modal-dialog-centered modal-lg">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<SmartForm Model="CreateForm" OnValidSubmit="FinishCreate">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<AutoForm Columns="6" Model="CreateForm"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</SmartModal>
|
||||
|
||||
<SmartModal @ref="UpdateModal" CssClasses="modal-dialog-centered modal-lg">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Update</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<SmartForm Model="UpdateForm" OnValidSubmit="FinishUpdate">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<AutoForm Columns="6" Model="UpdateForm"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</SmartModal>
|
||||
|
||||
<SmartModal @ref="DeleteModal" CssClasses="modal-dialog-centered">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Do you want to delete this item?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-gray-400 fs-5 fw-semibold">
|
||||
This action cannot be undone. The data will be deleted and cannot be restored
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer p-3">
|
||||
<div class="btn-group w-100">
|
||||
<WButton OnClick="FinishDelete" Text="Delete" CssClasses="btn btn-danger w-50 me-3"/>
|
||||
<button class="btn btn-secondary w-50" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</SmartModal>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
[Parameter]
|
||||
public Func<Repository<TItem>, TItem[]> Load { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? ValidateAdd { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? ValidateUpdate { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? ValidateDelete { get; set; }
|
||||
|
||||
private TItem[] Items;
|
||||
private TCreateForm CreateForm;
|
||||
private TUpdateForm UpdateForm;
|
||||
private TItem ItemToUpdate;
|
||||
private TItem ItemToDelete;
|
||||
|
||||
private SmartModal CreateModal;
|
||||
private SmartModal UpdateModal;
|
||||
private SmartModal DeleteModal;
|
||||
|
||||
private Expression<Func<TItem, object>> IdExpression;
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (Load == null)
|
||||
throw new ArgumentNullException(nameof(Load));
|
||||
|
||||
CreateForm = Activator.CreateInstance<TCreateForm>()!;
|
||||
UpdateForm = Activator.CreateInstance<TUpdateForm>()!;
|
||||
|
||||
IdExpression = CreateExpression();
|
||||
}
|
||||
|
||||
private Task LoadItems(LazyLoader _)
|
||||
{
|
||||
Items = Load.Invoke(ItemRepository);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task StartUpdate(TItem item)
|
||||
{
|
||||
UpdateForm = Mapper.Map<TUpdateForm>(item);
|
||||
ItemToUpdate = item;
|
||||
await UpdateModal.Show();
|
||||
}
|
||||
|
||||
private async Task FinishUpdate()
|
||||
{
|
||||
var item = Mapper.Map(ItemToUpdate, UpdateForm!);
|
||||
|
||||
if (ValidateUpdate != null) // Optional additional validation
|
||||
await ValidateUpdate.Invoke(item);
|
||||
|
||||
ItemRepository.Update(item);
|
||||
|
||||
// Reset
|
||||
await UpdateModal.Hide();
|
||||
await LazyLoader.Reload();
|
||||
await ToastService.Success("Successfully updated item");
|
||||
}
|
||||
|
||||
private async Task StartCreate()
|
||||
{
|
||||
CreateForm = Activator.CreateInstance<TCreateForm>()!;
|
||||
await CreateModal.Show();
|
||||
}
|
||||
|
||||
private async Task FinishCreate()
|
||||
{
|
||||
var item = Mapper.Map<TItem>(CreateForm!);
|
||||
|
||||
if (ValidateAdd != null) // Optional additional validation
|
||||
await ValidateAdd.Invoke(item);
|
||||
|
||||
ItemRepository.Add(item);
|
||||
|
||||
// Reset
|
||||
await CreateModal.Hide();
|
||||
await LazyLoader.Reload();
|
||||
await ToastService.Success("Successfully added item");
|
||||
}
|
||||
|
||||
private async Task StartDelete(TItem item)
|
||||
{
|
||||
ItemToDelete = item;
|
||||
await DeleteModal.Show();
|
||||
}
|
||||
|
||||
private async Task FinishDelete()
|
||||
{
|
||||
if (ValidateDelete != null) // Optional additional validation
|
||||
await ValidateDelete.Invoke(ItemToDelete);
|
||||
|
||||
ItemRepository.Delete(ItemToDelete);
|
||||
|
||||
// Reset
|
||||
await DeleteModal.Hide();
|
||||
await LazyLoader.Reload();
|
||||
await ToastService.Success("Successfully deleted item");
|
||||
}
|
||||
|
||||
private Expression<Func<TItem, object>> CreateExpression()
|
||||
{
|
||||
// Parameter expression for the input object
|
||||
var inputParam = Expression.Parameter(typeof(TItem), "input");
|
||||
|
||||
// Convert the input object to the actual model type (MyModel in this example)
|
||||
var castedInput = Expression.Convert(inputParam, typeof(TItem));
|
||||
|
||||
// Create a property access expression using the property name
|
||||
var propertyAccess = Expression.Property(castedInput, "Id");
|
||||
|
||||
// Convert the property value to an object
|
||||
var castedResult = Expression.Convert(propertyAccess, typeof(object));
|
||||
|
||||
// Create a lambda expression
|
||||
var lambda = Expression.Lambda<Func<TItem, object>>(castedResult, inputParam);
|
||||
|
||||
return lambda;
|
||||
}
|
||||
}
|
27
Moonlight/Shared/Components/Forms/AutoForm.razor
Normal file
27
Moonlight/Shared/Components/Forms/AutoForm.razor
Normal file
|
@ -0,0 +1,27 @@
|
|||
@using Moonlight.App.Extensions
|
||||
|
||||
@typeparam TForm
|
||||
|
||||
@foreach (var prop in typeof(TForm).GetProperties())
|
||||
{
|
||||
<div class="col-md-@(Columns) col-12">
|
||||
<CascadingValue Name="Property" Value="prop">
|
||||
<CascadingValue Name="Data" Value="(object)Model">
|
||||
@{
|
||||
var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType);
|
||||
}
|
||||
|
||||
@ComponentHelper.FromType(typeToCreate)
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public TForm Model { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int Columns { get; set; } = 6;
|
||||
}
|
151
Moonlight/Shared/Components/Forms/AutoProperty.razor
Normal file
151
Moonlight/Shared/Components/Forms/AutoProperty.razor
Normal file
|
@ -0,0 +1,151 @@
|
|||
@using System.Reflection
|
||||
@using System.ComponentModel
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Moonlight.App.Extensions.Attributes
|
||||
@using Moonlight.App.Repositories
|
||||
|
||||
@typeparam TProp
|
||||
@inject IServiceProvider ServiceProvider
|
||||
|
||||
<label class="form-label">
|
||||
@(Formatter.ConvertCamelCaseToSpaces(Property.Name))
|
||||
</label>
|
||||
|
||||
@* Description using attribute *@
|
||||
|
||||
@{
|
||||
var attrs = Property.GetCustomAttributes(true);
|
||||
|
||||
var descAttr = attrs
|
||||
.FirstOrDefault(x => x.GetType() == typeof(DescriptionAttribute));
|
||||
}
|
||||
|
||||
@if (descAttr != null)
|
||||
{
|
||||
var attribute = descAttr as DescriptionAttribute;
|
||||
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
@(attribute!.Description)
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Actual value binding *@
|
||||
|
||||
<div class="input-group mb-5">
|
||||
@if (Property.PropertyType == typeof(string))
|
||||
{
|
||||
<div class="w-100">
|
||||
<InputText id="@Property.Name" @bind-Value="Binder.StringValue" class="form-control"/>
|
||||
</div>
|
||||
}
|
||||
else if (Property.PropertyType == typeof(int))
|
||||
{
|
||||
<InputNumber id="@Property.Name" @bind-Value="Binder.IntValue" class="form-control"/>
|
||||
}
|
||||
else if (Property.PropertyType == typeof(double))
|
||||
{
|
||||
<InputNumber id="@Property.Name" @bind-Value="Binder.DoubleValue" class="form-control"/>
|
||||
}
|
||||
else if (Property.PropertyType == typeof(long))
|
||||
{
|
||||
<InputNumber id="@Property.Name" @bind-Value="Binder.LongValue" class="form-control"/>
|
||||
}
|
||||
else if (Property.PropertyType == typeof(bool))
|
||||
{
|
||||
<div class="form-check">
|
||||
<InputCheckbox id="@Property.Name" @bind-Value="Binder.BoolValue" class="form-check-input"/>
|
||||
</div>
|
||||
}
|
||||
else if (Property.PropertyType == typeof(DateTime))
|
||||
{
|
||||
<InputDate id="@Property.Name" @bind-Value="Binder.DateTimeValue" class="form-control"/>
|
||||
}
|
||||
else if (Property.PropertyType == typeof(decimal))
|
||||
{
|
||||
<InputNumber id="@Property.Name" step="0.01" @bind-Value="Binder.DoubleValue" class="form-control"/>
|
||||
}
|
||||
else if (Property.PropertyType.IsEnum)
|
||||
{
|
||||
<select @bind="Binder.Class" class="form-select">
|
||||
@foreach (var status in (TProp[])Enum.GetValues(typeof(TProp)))
|
||||
{
|
||||
if (Binder.Class.ToString() == status.ToString())
|
||||
{
|
||||
<option value="@(status)" selected="">@(status)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@(status)">@(status)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
}
|
||||
else if (Property.PropertyType.IsClass)
|
||||
{
|
||||
var attribute = Property.GetCustomAttributes(true)
|
||||
.FirstOrDefault(x => x.GetType() == typeof(SelectorAttribute)) as SelectorAttribute;
|
||||
|
||||
if (attribute != null)
|
||||
{
|
||||
if (attribute.UseDropdown)
|
||||
{
|
||||
var displayFunc = new Func<TProp, string>(x =>
|
||||
{
|
||||
var prop = typeof(TProp).GetProperties().First(x => x.Name == attribute.DisplayProp);
|
||||
return prop.GetValue(x) as string ?? "N/A";
|
||||
});
|
||||
|
||||
var searchFunc = new Func<TProp, string>(x =>
|
||||
{
|
||||
var prop = typeof(TProp).GetProperties().First(x => x.Name == attribute.SelectorProp);
|
||||
return prop.GetValue(x) as string ?? "N/A";
|
||||
});
|
||||
|
||||
<SmartDropdown @bind-Value="Binder.Class" DisplayFunc="displayFunc" SearchProp="searchFunc" Items="Items" />
|
||||
}
|
||||
else
|
||||
{
|
||||
var displayFunc = new Func<TProp, string>(x =>
|
||||
{
|
||||
var prop = typeof(TProp).GetProperties().First(x => x.Name == attribute.DisplayProp);
|
||||
return prop.GetValue(x) as string ?? "N/A";
|
||||
});
|
||||
|
||||
<SmartSelect @bind-Value="Binder.Class" DisplayField="displayFunc" Items="Items" CanBeNull="true" />
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter(Name = "Data")]
|
||||
public object Data { get; set; }
|
||||
|
||||
[CascadingParameter(Name = "Property")]
|
||||
public PropertyInfo Property { get; set; }
|
||||
|
||||
private PropBinder<TProp> Binder;
|
||||
private TProp[] Items = Array.Empty<TProp>();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Binder = new(Property, Data);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
if (Property.GetCustomAttributes(true).Any(x => x.GetType() == typeof(SelectorAttribute)))
|
||||
{
|
||||
var typeToGetByDi = typeof(Repository<>).MakeGenericType(typeof(TProp));
|
||||
var repo = ServiceProvider.GetRequiredService(typeToGetByDi);
|
||||
var dbSet = repo.GetType().GetMethods().First(x => x.Name == "Get").Invoke(repo, null) as IEnumerable<TProp>;
|
||||
Items = dbSet!.ToArray();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
141
Moonlight/Shared/Components/Forms/SmartDropdown.razor
Normal file
141
Moonlight/Shared/Components/Forms/SmartDropdown.razor
Normal file
|
@ -0,0 +1,141 @@
|
|||
@using Microsoft.AspNetCore.Components.Forms
|
||||
|
||||
@typeparam T
|
||||
@inherits InputBase<T>
|
||||
|
||||
<div class="dropdown w-100">
|
||||
<div class="input-group">
|
||||
@if (CurrentValue == null)
|
||||
{
|
||||
<input class="form-control" type="text" @bind-value="SearchTerm" @bind-value:event="oninput" placeholder="@(Placeholder)">
|
||||
}
|
||||
else
|
||||
{
|
||||
<input class="form-control" type="text" value="@(DisplayFunc(CurrentValue))">
|
||||
<button class="btn btn-sm btn-primary" @onclick="() => SelectItem(default(T)!)">
|
||||
<i class="bx bx-sm bx-x"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@{
|
||||
var anyItems = FilteredItems.Any();
|
||||
}
|
||||
|
||||
<div class="dropdown-menu w-100 @(anyItems ? "show" : "")" style="max-height: 200px; overflow-y: auto;">
|
||||
@if (anyItems)
|
||||
{
|
||||
foreach (var item in FilteredItems)
|
||||
{
|
||||
<button class="dropdown-item py-2" type="button" @onclick="() => SelectItem(item)">@DisplayFunc(item)</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public IEnumerable<T> Items { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<T, string> DisplayFunc { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<T, string> SearchProp { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<Task>? OnSelected { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Placeholder { get; set; } = "Search...";
|
||||
|
||||
private string SearchTerm
|
||||
{
|
||||
get => searchTerm;
|
||||
set
|
||||
{
|
||||
searchTerm = value;
|
||||
FilteredItems = Items.OrderByDescending(x => Matches(SearchProp(x), searchTerm)).Take(30).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private string searchTerm = "";
|
||||
|
||||
private List<T> FilteredItems = new();
|
||||
|
||||
private async void SelectItem(T item)
|
||||
{
|
||||
CurrentValue = item;
|
||||
SearchTerm = "";
|
||||
FilteredItems.Clear();
|
||||
|
||||
if (OnSelected != null)
|
||||
await OnSelected.Invoke();
|
||||
}
|
||||
|
||||
protected override bool TryParseValueFromString(string? value, out T result, out string? validationErrorMessage)
|
||||
{
|
||||
// Check if the value is null or empty
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
result = default(T)!;
|
||||
validationErrorMessage = "Value cannot be null or empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to find an item that matches the search term
|
||||
var item = FilteredItems.OrderByDescending(x => Matches(SearchProp(x), value)).FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
result = item;
|
||||
validationErrorMessage = null;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = default(T)!;
|
||||
validationErrorMessage = $"No item found for search term '{value}'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private float Matches(string input, string search)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(search))
|
||||
return 0;
|
||||
|
||||
var cleanedSearch = search
|
||||
.ToLower()
|
||||
.Replace(" ", "")
|
||||
.Replace("-", "");
|
||||
|
||||
var cleanedInput = input
|
||||
.ToLower()
|
||||
.Replace(" ", "")
|
||||
.Replace("-", "");
|
||||
|
||||
if (cleanedInput == cleanedSearch)
|
||||
return 10000;
|
||||
|
||||
float matches = 0;
|
||||
|
||||
int i = 0;
|
||||
foreach (var c in cleanedInput)
|
||||
{
|
||||
if (cleanedSearch.Length > i)
|
||||
{
|
||||
if (c == cleanedSearch[i])
|
||||
matches++;
|
||||
else
|
||||
matches--;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
matches = matches / searchTerm.Length;
|
||||
|
||||
return matches;
|
||||
}
|
||||
}
|
27
Moonlight/Shared/Components/Forms/SmartEnumSelect.razor
Normal file
27
Moonlight/Shared/Components/Forms/SmartEnumSelect.razor
Normal file
|
@ -0,0 +1,27 @@
|
|||
@typeparam TField where TField : struct
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inherits InputBase<TField>
|
||||
|
||||
<select @bind="CurrentValue" class="form-select">
|
||||
@foreach (var status in (TField[])Enum.GetValues(typeof(TField)))
|
||||
{
|
||||
if (CurrentValue.ToString() == status.ToString())
|
||||
{
|
||||
<option value="@(status)" selected="">@(status)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@(status)">@(status)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
|
||||
@code
|
||||
{
|
||||
protected override bool TryParseValueFromString(string? value, out TField result, out string? validationErrorMessage)
|
||||
{
|
||||
result = Enum.Parse<TField>(value);
|
||||
validationErrorMessage = "";
|
||||
return false;
|
||||
}
|
||||
}
|
71
Moonlight/Shared/Components/Forms/SmartSelect.razor
Normal file
71
Moonlight/Shared/Components/Forms/SmartSelect.razor
Normal file
|
@ -0,0 +1,71 @@
|
|||
@typeparam TField
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inherits InputBase<TField>
|
||||
|
||||
<select class="form-select" @bind="Binding">
|
||||
@if (CanBeNull)
|
||||
{
|
||||
<option value="-1">---</option>
|
||||
}
|
||||
@foreach (var item in Items)
|
||||
{
|
||||
<option value="@(item!.GetHashCode())">@(DisplayField(item))</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public IEnumerable<TField> Items { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<TField, string> DisplayField { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<Task>? OnChange { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool CanBeNull { get; set; } = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override string? FormatValueAsString(TField? value)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
return DisplayField.Invoke(value);
|
||||
}
|
||||
|
||||
protected override bool TryParseValueFromString(string? value, out TField result, out string? validationErrorMessage)
|
||||
{
|
||||
validationErrorMessage = "";
|
||||
result = default(TField)!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private int Binding
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Value == null)
|
||||
return -1;
|
||||
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
set
|
||||
{
|
||||
var i = Items.FirstOrDefault(x => x!.GetHashCode() == value);
|
||||
|
||||
if(i == null && !CanBeNull)
|
||||
return;
|
||||
|
||||
Value = i;
|
||||
ValueChanged.InvokeAsync(i);
|
||||
OnChange?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
105
Moonlight/Shared/Components/Modals/ManageServiceShareModal.razor
Normal file
105
Moonlight/Shared/Components/Modals/ManageServiceShareModal.razor
Normal file
|
@ -0,0 +1,105 @@
|
|||
@using Moonlight.App.Services.ServiceManage
|
||||
@using Moonlight.App.Database.Entities.Store
|
||||
@using BlazorTable
|
||||
@using Moonlight.App.Models.Forms.Services
|
||||
|
||||
@inject ServiceService ServiceService
|
||||
|
||||
<SmartModal @ref="Modal" CssClasses="modal-dialog-centered modal-lg">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fs-3">Manage shared users</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
<div class="mb-3">
|
||||
<SmartForm Model="Form" OnValidSubmit="Add">
|
||||
<div class="input-group">
|
||||
<input @bind="Form.Username" type="text" placeholder="Enter a username..." class="form-control"/>
|
||||
<button type="submit" class="btn btn-primary">Add</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</div>
|
||||
@if (Users.Any())
|
||||
{
|
||||
<Table TableItem="User"
|
||||
Items="Users"
|
||||
PageSize="50"
|
||||
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||
TableHeadClass="fw-bold text-muted">
|
||||
<Column TableItem="User" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<div class="symbol symbol-50px me-5">
|
||||
@if (context.Avatar == null)
|
||||
{
|
||||
<img src="/assets/img/avatar.png" alt="Avatar">
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/api/bucket/avatars/@(context.Avatar)" alt="Avatar">
|
||||
}
|
||||
</div>
|
||||
<span class="ms-3">@(context.Username)</span>
|
||||
</Template>
|
||||
</Column>
|
||||
<Column TableItem="User" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<div class="text-end me-3">
|
||||
<a @onclick="() => Remove(context)" @onclick:preventDefault href="#" class="text-danger">Remove</a>
|
||||
</div>
|
||||
</Template>
|
||||
</Column>
|
||||
</Table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex justify-content-center py-4">
|
||||
<span class="mt-3 fs-5">No users found</span>
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</SmartModal>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public Service Service { get; set; }
|
||||
|
||||
private SmartModal Modal;
|
||||
private LazyLoader LazyLoader;
|
||||
private User[] Users;
|
||||
|
||||
private AddUserToServiceForm Form = new();
|
||||
|
||||
private async Task Load(LazyLoader _)
|
||||
{
|
||||
Users = await ServiceService.GetSharedUsers(Service);
|
||||
}
|
||||
|
||||
private async Task Add()
|
||||
{
|
||||
await ServiceService.AddSharedUser(Service, Form.Username);
|
||||
Form = new();
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task Remove(User user)
|
||||
{
|
||||
await ServiceService.RemoveSharedUser(Service, user);
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
public async Task Show()
|
||||
{
|
||||
await Modal.Show();
|
||||
}
|
||||
|
||||
public async Task Hide()
|
||||
{
|
||||
await Modal.Hide();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
@using Moonlight.App.Models.Forms.Store
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Database.Entities.Store
|
||||
@using Mappy.Net
|
||||
@using Moonlight.App.Models.Forms.Admin.Store
|
||||
|
||||
@inject Repository<Coupon> CouponRepository
|
||||
@inject ToastService ToastService
|
||||
|
||||
<SmartModal @ref="Modal" CssClasses="modal-dialog-centered modal-lg">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fs-3">Add new modal</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<SmartForm Model="Form" OnValidSubmit="OnValidSubmit">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Code</label>
|
||||
<input @bind="Form.Code" class="form-control" type="text"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Percent</label>
|
||||
<input @bind="Form.Percent" class="form-control" type="number"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Amount</label>
|
||||
<input @bind="Form.Amount" class="form-control" type="number"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</SmartModal>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public Func<Task> OnUpdate { get; set; }
|
||||
|
||||
private SmartModal Modal;
|
||||
private AddCouponForm Form = new();
|
||||
|
||||
public async Task Show()
|
||||
{
|
||||
await Modal.Show();
|
||||
}
|
||||
|
||||
private async Task OnValidSubmit()
|
||||
{
|
||||
var coupon = Mapper.Map<Coupon>(Form);
|
||||
CouponRepository.Add(coupon);
|
||||
|
||||
Form = new();
|
||||
await ToastService.Success("Successfully added new coupon");
|
||||
await Modal.Hide();
|
||||
await OnUpdate.Invoke();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<div class="card mb-5 mb-xl-10">
|
||||
<div class="card mb-5 mb-xl-10">
|
||||
<div class="card-body pt-0 pb-0">
|
||||
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
|
||||
<li class="nav-item mt-2">
|
||||
|
@ -11,6 +11,11 @@
|
|||
Security
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item mt-2">
|
||||
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/account/payments">
|
||||
Payments
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<div class="card mb-5 mb-xl-10">
|
||||
<div class="card-body pt-0 pb-0">
|
||||
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
|
||||
<li class="nav-item mt-2">
|
||||
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/store">
|
||||
<i class="bx bx-sm bxs-shopping-bags me-2"></i> Overview
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item mt-2">
|
||||
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/store/coupons">
|
||||
<i class="bx bx-sm bxs-coupon me-2"></i> Coupons
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item mt-2">
|
||||
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/admin/store/gifts">
|
||||
<i class="bx bx-sm bx-gift me-2"></i> Gifts
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public int Index { get; set; }
|
||||
}
|
|
@ -1,34 +1,49 @@
|
|||
@if (loaded)
|
||||
@if (Loaded)
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex justify-content-center py-4">
|
||||
<span class="fs-1 spinner-border spinner-border-lg align-middle me-2"></span>
|
||||
<span class="mt-3 fs-5">@(Text)</span>
|
||||
</div>
|
||||
if (ShowAsCard)
|
||||
{
|
||||
<div class="card card-body">
|
||||
<div class="d-flex justify-content-center py-4">
|
||||
<span class="fs-1 spinner-border spinner-border-lg align-middle me-2"></span>
|
||||
<span class="mt-3 fs-5">@(Text)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex justify-content-center py-4">
|
||||
<span class="fs-1 spinner-border spinner-border-lg align-middle me-2"></span>
|
||||
<span class="mt-3 fs-5">@(Text)</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
|
||||
[Parameter]
|
||||
public bool ShowAsCard { get; set; } = false;
|
||||
|
||||
[Parameter]
|
||||
public Func<LazyLoader, Task> Load { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
private bool loaded = false;
|
||||
private bool Loaded = false;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await Load.Invoke(this);
|
||||
loaded = true;
|
||||
Loaded = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
@ -41,10 +56,10 @@ else
|
|||
|
||||
public async Task Reload()
|
||||
{
|
||||
loaded = false;
|
||||
Loaded = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await Load.Invoke(this);
|
||||
loaded = true;
|
||||
Loaded = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
@using Moonlight.App.Services
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject ConfigService ConfigService
|
||||
@inject CookieService CookieService
|
||||
|
||||
<div class="app-header">
|
||||
|
@ -57,6 +58,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="separator my-2"></div>
|
||||
<div class="menu-item px-5">
|
||||
<a href="/account/payments" class="menu-link px-5">
|
||||
<span class="menu-badge">
|
||||
<span class="badge badge-info fw-bold fs-7 badge-lg">@(ConfigService.Get().Store.Currency) @(IdentityService.CurrentUser.Balance)</span>
|
||||
</span>
|
||||
<span class="menu-text ms-3">
|
||||
Payment
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="separator my-2"></div>
|
||||
<div class="menu-item px-5">
|
||||
<a href="/account" class="menu-link px-5">
|
||||
Account
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
</div>
|
||||
<div class="app-sidebar-separator separator"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="menu-item">
|
||||
<a class="menu-link " href="/admin">
|
||||
<span class="menu-icon">
|
||||
|
@ -73,7 +73,7 @@
|
|||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="menu-item">
|
||||
<a class="menu-link " href="/admin/users">
|
||||
<span class="menu-icon">
|
||||
|
@ -84,6 +84,17 @@
|
|||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="menu-item">
|
||||
<a class="menu-link " href="/admin/store">
|
||||
<span class="menu-icon">
|
||||
<i class="bx bx-sm bxs-shopping-bags"></i>
|
||||
</span>
|
||||
<span class="menu-title">
|
||||
Store
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
45
Moonlight/Shared/Components/Partials/SmartModal.razor
Normal file
45
Moonlight/Shared/Components/Partials/SmartModal.razor
Normal file
|
@ -0,0 +1,45 @@
|
|||
@inject ModalService ModalService
|
||||
|
||||
<div class="modal fade" id="modal@(Id)" tabindex="-1">
|
||||
<div class="modal-dialog @(CssClasses)">
|
||||
<div class="modal-content">
|
||||
@if (ShouldShow)
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public string CssClasses { get; set; } = "";
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
private int Id;
|
||||
private bool ShouldShow = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Id = GetHashCode();
|
||||
}
|
||||
|
||||
public async Task Show()
|
||||
{
|
||||
ShouldShow = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await ModalService.Show("modal" + Id);
|
||||
}
|
||||
|
||||
public async Task Hide()
|
||||
{
|
||||
await ModalService.Hide("modal" + Id);
|
||||
|
||||
ShouldShow = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
251
Moonlight/Shared/Components/Service/ServiceItem.razor
Normal file
251
Moonlight/Shared/Components/Service/ServiceItem.razor
Normal file
|
@ -0,0 +1,251 @@
|
|||
@using Moonlight.App.Database.Entities.Store
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.ServiceManage
|
||||
@using Moonlight.App.Services.Store
|
||||
@using Moonlight.Shared.Components.Modals
|
||||
|
||||
@inject ConfigService ConfigService
|
||||
@inject ToastService ToastService
|
||||
@inject ServiceService ServiceService
|
||||
@inject StoreService StoreService
|
||||
@inject IdentityService IdentityService
|
||||
|
||||
@if (ShowDeletionScreen)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title align-items-start flex-column">
|
||||
<span class="card-label fw-bold text-dark fs-3">Do you really want to delete @(Service.Nickname ?? $"Service {Service.Id}")</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-gray-400 fs-5 fw-semibold">
|
||||
This action cannot be undone. Your service data will be deleted and cannot be restored
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer p-3">
|
||||
<div class="btn-group w-100">
|
||||
<WButton OnClick="Delete" Text="Delete" CssClasses="btn btn-danger w-50 me-3"/>
|
||||
<button @onclick="() => SetShowDeletion(false)" class="btn btn-secondary w-50">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (ShowRenewScreen)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title align-items-start flex-column">
|
||||
<span class="card-label fw-bold text-dark fs-3">Do you want to renew @(Service.Nickname ?? $"Service {Service.Id}")</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body fs-5">
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-4 col-12">
|
||||
<a @onclick="() => SetDurationMultiplier(1)" @onclick:preventDefault href="#" class="card card-body bg-hover-secondary text-center @(DurationMultiplier == 1 ? "border border-info" : "")">
|
||||
<h6 class="fw-bold mb-0 align-middle">@(Service.Product.Duration * 1) days</h6>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
<a @onclick="() => SetDurationMultiplier(2)" @onclick:preventDefault href="#" class="card card-body bg-hover-secondary text-center @(DurationMultiplier == 2 ? "border border-info" : "")">
|
||||
<h6 class="fw-bold mb-0 align-middle">@(Service.Product.Duration * 2) days</h6>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
<a @onclick="() => SetDurationMultiplier(3)" @onclick:preventDefault href="#" class="card card-body bg-hover-secondary text-center @(DurationMultiplier == 3 ? "border border-info" : "")">
|
||||
<h6 class="fw-bold mb-0 align-middle">@(Service.Product.Duration * 3) days</h6>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@{
|
||||
var actualPrice = Service.Product.Price * DurationMultiplier;
|
||||
var currency = ConfigService.Get().Store.Currency;
|
||||
}
|
||||
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Today</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(currency) @(actualPrice)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Renew</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(currency) @(Service.Product.Price)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Duration</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(Service.Product.Duration * DurationMultiplier) days</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator my-4"></div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Total</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(currency) @(actualPrice)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-7">
|
||||
@if (!CanBeRenewed && !string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
<div class="alert alert-warning bg-warning text-white text-center">
|
||||
@ErrorMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
@if (IsValidating)
|
||||
{
|
||||
<button class="btn btn-primary w-100 disabled" disabled="">
|
||||
<span class="spinner-border spinner-border-sm align-middle"></span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CanBeRenewed)
|
||||
{
|
||||
<WButton OnClick="Renew" Text="@($"Renew for {currency} {actualPrice}")" CssClasses="btn btn-primary w-100"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-primary w-100 disabled" disabled="">Order for @(currency) @(actualPrice)</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer p-3">
|
||||
<div class="btn-group w-100">
|
||||
<button @onclick="() => SetShowRenew(false)" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title align-items-start flex-column">
|
||||
<span class="card-label fw-bold text-dark fs-3">@(Service.Nickname ?? $"Service {Service.Id}")</span>
|
||||
<span class="text-gray-400 mt-1 fw-semibold fs-6">@(Service.Product.Name)</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body fs-6">
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Renew price</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(ConfigService.Get().Store.Currency) @(Service.Product.Price)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Renew at</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(Formatter.FormatDate(Service.RenewAt))</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Created at</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(Formatter.FormatDate(Service.CreatedAt))</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer p-3">
|
||||
<div class="btn-group w-100 mb-3">
|
||||
<a href="/service/@(Service.Id)" class="btn btn-primary w-50 me-3">Manage</a>
|
||||
<button @onclick="() => ShareModal.Show()" class="btn btn-secondary w-50">Manage shares</button>
|
||||
</div>
|
||||
<div class="btn-group w-100">
|
||||
<button @onclick="() => SetShowRenew(true)" class="btn btn-warning w-50 me-3">Renew</button>
|
||||
<button @onclick="() => SetShowDeletion(true)" class="btn btn-danger w-50">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ManageServiceShareModal @ref="ShareModal" Service="Service"/>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public Service Service { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<Task> OnChange { get; set; }
|
||||
|
||||
private bool ShowDeletionScreen = false;
|
||||
private bool ShowRenewScreen = false;
|
||||
private ManageServiceShareModal ShareModal;
|
||||
|
||||
private int DurationMultiplier = 1;
|
||||
private bool CanBeRenewed = false;
|
||||
private bool IsValidating = false;
|
||||
private string ErrorMessage = "";
|
||||
|
||||
private Task Revalidate()
|
||||
{
|
||||
IsValidating = true;
|
||||
InvokeAsync(StateHasChanged);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await StoreService.Order.ValidateRenew(IdentityService.CurrentUser, Service, DurationMultiplier);
|
||||
CanBeRenewed = true;
|
||||
}
|
||||
catch (DisplayException e)
|
||||
{
|
||||
CanBeRenewed = false;
|
||||
ErrorMessage = e.Message;
|
||||
}
|
||||
|
||||
IsValidating = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Renew()
|
||||
{
|
||||
await StoreService.Order.Renew(IdentityService.CurrentUser, Service, DurationMultiplier);
|
||||
|
||||
await ToastService.Success("Successfully renewed service");
|
||||
await OnChange.Invoke();
|
||||
}
|
||||
|
||||
private async Task SetDurationMultiplier(int i)
|
||||
{
|
||||
DurationMultiplier = i;
|
||||
await Revalidate();
|
||||
}
|
||||
|
||||
private async Task SetShowDeletion(bool b)
|
||||
{
|
||||
ShowDeletionScreen = b;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task SetShowRenew(bool b)
|
||||
{
|
||||
ShowRenewScreen = b;
|
||||
|
||||
if (b) // Revalidate when the renew screen is shown
|
||||
await Revalidate();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async Task Delete()
|
||||
{
|
||||
await ServiceService.Admin.Delete(Service);
|
||||
|
||||
await ToastService.Success("Successfully deleted service");
|
||||
await OnChange.Invoke();
|
||||
}
|
||||
}
|
316
Moonlight/Shared/Components/Store/StoreModals.razor
Normal file
316
Moonlight/Shared/Components/Store/StoreModals.razor
Normal file
|
@ -0,0 +1,316 @@
|
|||
@using Moonlight.App.Services.Store
|
||||
@using Moonlight.App.Models.Forms.Store
|
||||
@using Moonlight.App.Database.Enums
|
||||
@using Moonlight.App.Database.Entities.Store
|
||||
@using Moonlight.App.Repositories
|
||||
@using Mappy.Net
|
||||
|
||||
@inject StoreService StoreService
|
||||
@inject ToastService ToastService
|
||||
@inject Repository<Category> CategoryRepository
|
||||
|
||||
<SmartModal @ref="AddCategoryModal" CssClasses="modal-dialog-centered">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add new category</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<SmartForm Model="AddCategoryForm" OnValidSubmit="AddCategorySubmit">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input @bind="AddCategoryForm.Name" class="form-control" type="text"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea @bind="AddCategoryForm.Description" class="form-control" type="text"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Slug</label>
|
||||
<input @bind="AddCategoryForm.Slug" class="form-control" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</SmartModal>
|
||||
|
||||
<SmartModal @ref="EditCategoryModal" CssClasses="modal-dialog-centered">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit category</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<SmartForm Model="EditCategoryForm" OnValidSubmit="EditCategorySubmit">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input @bind="EditCategoryForm.Name" class="form-control" type="text"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea @bind="EditCategoryForm.Description" class="form-control" type="text"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Slug</label>
|
||||
<input @bind="EditCategoryForm.Slug" class="form-control" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</SmartModal>
|
||||
|
||||
<SmartModal @ref="AddProductModal" CssClasses="modal-dialog-centered modal-lg">
|
||||
<LazyLoader Load="LoadCategories">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add new product</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<SmartForm Model="AddProductForm" OnValidSubmit="AddProductSubmit">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input @bind="AddProductForm.Name" class="form-control" type="text"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea @bind="AddProductForm.Description" class="form-control" type="text"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Slug</label>
|
||||
<input @bind="AddProductForm.Slug" class="form-control" type="text"/>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label class="form-label">Category</label>
|
||||
<SmartSelect @bind-Value="AddProductForm.Category" Items="Categories" DisplayField="@(x => x.Name)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Price</label>
|
||||
<input @bind="AddProductForm.Price" class="form-control" type="text"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Duration</label>
|
||||
<input @bind="AddProductForm.Duration" class="form-control" type="number"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max instances per user</label>
|
||||
<input @bind="AddProductForm.MaxPerUser" class="form-control" type="number"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Stock</label>
|
||||
<input @bind="AddProductForm.Stock" class="form-control" type="number"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<SmartEnumSelect @bind-Value="AddProductForm.Type"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Config</label>
|
||||
<input @bind="AddProductForm.ConfigJson" class="form-control" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</LazyLoader>
|
||||
</SmartModal>
|
||||
|
||||
<SmartModal @ref="EditProductModal" CssClasses="modal-dialog-centered modal-lg">
|
||||
<LazyLoader Load="LoadCategories">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit product</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<SmartForm Model="EditProductForm" OnValidSubmit="EditProductSubmit">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input @bind="EditProductForm.Name" class="form-control" type="text"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea @bind="EditProductForm.Description" class="form-control" type="text"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Slug</label>
|
||||
<input @bind="EditProductForm.Slug" class="form-control" type="text"/>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label class="form-label">Category</label>
|
||||
<SmartSelect @bind-Value="EditProductForm.Category" Items="Categories" DisplayField="@(x => x.Name)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Price</label>
|
||||
<input @bind="EditProductForm.Price" class="form-control" type="text"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Duration</label>
|
||||
<input @bind="EditProductForm.Duration" class="form-control" type="number"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max instances per user</label>
|
||||
<input @bind="EditProductForm.MaxPerUser" class="form-control" type="number"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Stock</label>
|
||||
<input @bind="EditProductForm.Stock" class="form-control" type="number"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<SmartEnumSelect @bind-Value="EditProductForm.Type"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Config</label>
|
||||
<input @bind="EditProductForm.ConfigJson" class="form-control" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</LazyLoader>
|
||||
</SmartModal>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public Func<Task> OnUpdate { get; set; }
|
||||
|
||||
#region Add category
|
||||
|
||||
private SmartModal AddCategoryModal;
|
||||
private AddCategoryForm AddCategoryForm = new();
|
||||
|
||||
public Task AddCategoryShow => AddCategoryModal.Show();
|
||||
|
||||
private async Task AddCategorySubmit()
|
||||
{
|
||||
await StoreService.Admin.AddCategory(
|
||||
AddCategoryForm.Name,
|
||||
AddCategoryForm.Description,
|
||||
AddCategoryForm.Slug
|
||||
);
|
||||
|
||||
await ToastService.Success("Successfully added category");
|
||||
await AddCategoryModal.Hide();
|
||||
|
||||
AddCategoryForm = new();
|
||||
await OnUpdate.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edit category
|
||||
|
||||
private SmartModal EditCategoryModal;
|
||||
private EditCategoryForm EditCategoryForm = new();
|
||||
private Category EditCategory;
|
||||
|
||||
public async Task EditCategoryShow(Category category)
|
||||
{
|
||||
EditCategory = category;
|
||||
|
||||
EditCategoryForm = Mapper.Map<EditCategoryForm>(EditCategory);
|
||||
await EditCategoryModal.Show();
|
||||
}
|
||||
private async Task EditCategorySubmit()
|
||||
{
|
||||
EditCategory = Mapper.Map(EditCategory, EditCategoryForm);
|
||||
|
||||
await StoreService.Admin.UpdateCategory(EditCategory);
|
||||
|
||||
await ToastService.Success("Successfully updated category");
|
||||
await EditCategoryModal.Hide();
|
||||
|
||||
await OnUpdate.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Add product
|
||||
|
||||
private SmartModal AddProductModal;
|
||||
private AddProductForm AddProductForm = new();
|
||||
private Category[] Categories;
|
||||
|
||||
public Task AddProductShow => AddProductModal.Show();
|
||||
|
||||
private async Task AddProductSubmit()
|
||||
{
|
||||
await StoreService.Admin.AddProduct(
|
||||
AddProductForm.Name,
|
||||
AddProductForm.Description,
|
||||
AddProductForm.Slug,
|
||||
AddProductForm.Type,
|
||||
AddProductForm.ConfigJson,
|
||||
product =>
|
||||
{
|
||||
product.Category = AddProductForm.Category;
|
||||
product.Duration = AddProductForm.Duration;
|
||||
product.Price = AddProductForm.Price;
|
||||
product.Stock = AddProductForm.Stock;
|
||||
product.MaxPerUser = AddProductForm.MaxPerUser;
|
||||
}
|
||||
);
|
||||
|
||||
await ToastService.Success("Successfully added product");
|
||||
await AddProductModal.Hide();
|
||||
|
||||
AddProductForm = new();
|
||||
await OnUpdate.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edit product
|
||||
|
||||
private SmartModal EditProductModal;
|
||||
private EditProductForm EditProductForm = new();
|
||||
private Product EditProduct;
|
||||
|
||||
public async Task EditProductShow(Product product)
|
||||
{
|
||||
EditProduct = product;
|
||||
|
||||
EditProductForm = Mapper.Map<EditProductForm>(EditProduct);
|
||||
await EditProductModal.Show();
|
||||
}
|
||||
|
||||
private async Task EditProductSubmit()
|
||||
{
|
||||
EditProduct = Mapper.Map(EditProduct, EditProductForm);
|
||||
|
||||
await StoreService.Admin.UpdateProduct(EditProduct);
|
||||
|
||||
await ToastService.Success("Successfully updated product");
|
||||
await EditProductModal.Hide();
|
||||
|
||||
await OnUpdate.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Task LoadCategories(LazyLoader _)
|
||||
{
|
||||
Categories = CategoryRepository.Get().ToArray();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.Users
|
||||
@using Moonlight.App.Models.Forms
|
||||
@using Moonlight.App.Models.Forms.Auth
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject UserService UserService
|
||||
|
|
142
Moonlight/Shared/Views/Account/Payments.razor
Normal file
142
Moonlight/Shared/Views/Account/Payments.razor
Normal file
|
@ -0,0 +1,142 @@
|
|||
@page "/account/payments"
|
||||
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities.Store
|
||||
@using BlazorTable
|
||||
@using Moonlight.App.Models.Abstractions
|
||||
@using Moonlight.App.Services.Store
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject ConfigService ConfigService
|
||||
@inject StoreService StoreService
|
||||
@inject ToastService ToastService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<AccountNavigation Index="2"/>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Redeem gift code</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="input-group">
|
||||
<input @bind="GiftCode" class="form-control form-control-solid-bg" placeholder="Gift code..."/>
|
||||
<WButton OnClick="RedeemGiftCode" Text="Redeem" WorkingText="Checking" CssClasses="btn btn-primary"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body">
|
||||
<div class="row d-flex justify-content-center">
|
||||
<input @bind="Amount" class="form-control form-control-lg form-control-solid-bg w-50 text-center fs-2" placeholder="Enter amount to add to your account"/>
|
||||
</div>
|
||||
<div class="row mt-5 d-flex justify-content-center">
|
||||
@foreach (var gateway in StoreService.Payment.Gateways)
|
||||
{
|
||||
<div class="col-md-4 col-12">
|
||||
<a @onclick="() => SelectGateway(gateway)" @onclick:preventDefault href="#" class="card card-body bg-hover-info text-center border border-info @(SelectedGateway == gateway ? "bg-info" : "")">
|
||||
<i class="@(gateway.Icon)"></i>
|
||||
<h4 class="fw-bold mb-0 align-middle">@(gateway.Name)</h4>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<div class="text-center">
|
||||
<WButton OnClick="LaunchPayment" Text="Add balance to your account" CssClasses="btn btn-primary"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Transactions</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
<Table TableItem="Transaction"
|
||||
Items="Transactions"
|
||||
PageSize="50"
|
||||
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||
TableHeadClass="fw-bold text-muted">
|
||||
<Column TableItem="Transaction" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false" Width="10%">
|
||||
<Template>
|
||||
@if (context.Price == 0)
|
||||
{
|
||||
<i class="bx bx-sm bx-circle text-info align-middle"></i>
|
||||
}
|
||||
else if (context.Price < 0)
|
||||
{
|
||||
<i class="bx bx-sm bx-minus text-danger align-middle"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bx bx-sm bx-plus text-success align-middle"></i>
|
||||
}
|
||||
</Template>
|
||||
</Column>
|
||||
<Column TableItem="Transaction" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<span>@(ConfigService.Get().Store.Currency) @(Math.Abs(context.Price))</span>
|
||||
</Template>
|
||||
</Column>
|
||||
<Column TableItem="Transaction" Title="" Field="@(x => x.Text)" Sortable="false" Filterable="false"/>
|
||||
<Pager AlwaysShow="true"/>
|
||||
</Table>
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
private Transaction[] Transactions;
|
||||
private string GiftCode = "";
|
||||
private double Amount = 0;
|
||||
private PaymentGateway? SelectedGateway;
|
||||
|
||||
private async Task Load(LazyLoader _)
|
||||
{
|
||||
Transactions = await IdentityService.GetTransactions();
|
||||
|
||||
Transactions = Transactions
|
||||
.OrderByDescending(x => x.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private async Task RedeemGiftCode()
|
||||
{
|
||||
if (string.IsNullOrEmpty(GiftCode))
|
||||
return;
|
||||
|
||||
await StoreService.Gift.Redeem(IdentityService.CurrentUser, GiftCode);
|
||||
|
||||
// Reset
|
||||
GiftCode = "";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await ToastService.Success("Successfully applied gift code");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task SelectGateway(PaymentGateway gateway)
|
||||
{
|
||||
SelectedGateway = gateway;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task LaunchPayment()
|
||||
{
|
||||
if (SelectedGateway == null)
|
||||
return;
|
||||
|
||||
var url = await SelectedGateway.Start(Amount);
|
||||
Navigation.NavigateTo(url, true);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
@using OtpNet
|
||||
@using QRCoder
|
||||
@using Moonlight.App.Models.Enums
|
||||
@using Moonlight.App.Models.Forms.Auth
|
||||
|
||||
@inject IdentityService IdentityService
|
||||
@inject UserService UserService
|
||||
|
|
43
Moonlight/Shared/Views/Admin/Store/Coupons.razor
Normal file
43
Moonlight/Shared/Views/Admin/Store/Coupons.razor
Normal file
|
@ -0,0 +1,43 @@
|
|||
@page "/admin/store/coupons"
|
||||
|
||||
@using Moonlight.App.Database.Entities.Store
|
||||
@using Moonlight.App.Repositories
|
||||
@using BlazorTable
|
||||
@using Moonlight.App.Models.Forms.Admin.Store
|
||||
|
||||
@inject Repository<Coupon> CouponRepository
|
||||
|
||||
<AdminStoreNavigation Index="1"/>
|
||||
|
||||
<div class="mt-5">
|
||||
<AutoCrud TItem="Coupon"
|
||||
TCreateForm="AddCouponForm"
|
||||
TUpdateForm="EditCouponForm"
|
||||
Title="Manage coupons"
|
||||
Load="LoadData"
|
||||
ValidateAdd="Validate"
|
||||
ValidateUpdate="Validate">
|
||||
<Column TableItem="Coupon" Title="Id" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Coupon" Title="Code" Field="@(x => x.Code)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Coupon" Title="Amount" Field="@(x => x.Amount)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Coupon" Title="Percent" Field="@(x => x.Percent)" Sortable="true" Filterable="true"/>
|
||||
</AutoCrud>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private Coupon[] LoadData(Repository<Coupon> repository)
|
||||
{
|
||||
return repository
|
||||
.Get()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private Task Validate(Coupon coupon)
|
||||
{
|
||||
if (CouponRepository.Get().Any(x => x.Code == coupon.Code && x.Id != coupon.Id))
|
||||
throw new DisplayException("A coupon with that code does already exist");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
41
Moonlight/Shared/Views/Admin/Store/Gifts.razor
Normal file
41
Moonlight/Shared/Views/Admin/Store/Gifts.razor
Normal file
|
@ -0,0 +1,41 @@
|
|||
@page "/admin/store/gifts"
|
||||
|
||||
@using Moonlight.App.Database.Entities.Store
|
||||
@using Moonlight.App.Models.Forms.Admin.Store
|
||||
@using Moonlight.App.Repositories
|
||||
@using BlazorTable
|
||||
|
||||
@inject Repository<GiftCode> GiftCodeRepository
|
||||
|
||||
<AdminStoreNavigation Index="2"/>
|
||||
|
||||
<div class="mt-5">
|
||||
<AutoCrud TItem="GiftCode"
|
||||
TCreateForm="AddGiftCodeForm"
|
||||
TUpdateForm="EditGiftCodeForm"
|
||||
Title="Manage gift codes"
|
||||
Load="LoadData"
|
||||
ValidateAdd="Validate">
|
||||
<Column TableItem="GiftCode" Field="@(x => x.Code)" Title="Code" Sortable="false" Filterable="true" />
|
||||
<Column TableItem="GiftCode" Field="@(x => x.Amount)" Title="Amount" Sortable="true" Filterable="true" />
|
||||
<Column TableItem="GiftCode" Field="@(x => x.Value)" Title="Value" Sortable="true" Filterable="true" />
|
||||
</AutoCrud>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private GiftCode[] LoadData(Repository<GiftCode> repository)
|
||||
{
|
||||
return repository
|
||||
.Get()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private Task Validate(GiftCode giftCode)
|
||||
{
|
||||
if (GiftCodeRepository.Get().Any(x => x.Code == giftCode.Code && x.Id != giftCode.Id))
|
||||
throw new DisplayException("A gift code with that code does already exist");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
3
Moonlight/Shared/Views/Admin/Store/Index.razor
Normal file
3
Moonlight/Shared/Views/Admin/Store/Index.razor
Normal file
|
@ -0,0 +1,3 @@
|
|||
@page "/admin/store"
|
||||
|
||||
<AdminStoreNavigation Index="0"/>
|
66
Moonlight/Shared/Views/Services/Index.razor
Normal file
66
Moonlight/Shared/Views/Services/Index.razor
Normal file
|
@ -0,0 +1,66 @@
|
|||
@page "/services"
|
||||
|
||||
@using Moonlight.App.Services.ServiceManage
|
||||
@using Moonlight.App.Database.Entities.Store
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.Shared.Components.Service
|
||||
|
||||
@inject ServiceService ServiceService
|
||||
@inject IdentityService IdentityService
|
||||
|
||||
<LazyLoader @ref="LazyLoader" ShowAsCard="true" Load="Load">
|
||||
<div class="row mb-5">
|
||||
@foreach (var service in MyServices)
|
||||
{
|
||||
<div class="col-md-3 col-12">
|
||||
<ServiceItem Service="service" OnChange="LazyLoader.Reload" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@foreach (var service in SharedServices)
|
||||
{
|
||||
<div class="col-md-3 col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title align-items-start flex-column">
|
||||
<span class="card-label fw-bold text-dark fs-3">@(service.Nickname ?? $"Service {service.Id}")</span>
|
||||
<span class="text-gray-400 mt-1 fw-semibold fs-6">@(service.Product.Name)</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body fs-6">
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Shared by</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(service.Owner.Username)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Created at</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(Formatter.FormatDate(service.CreatedAt))</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer p-3 text-center">
|
||||
<button class="btn btn-primary">Manage</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
private Service[] MyServices;
|
||||
private Service[] SharedServices;
|
||||
|
||||
private async Task Load(LazyLoader _)
|
||||
{
|
||||
MyServices = await ServiceService.Get(IdentityService.CurrentUser);
|
||||
SharedServices = await ServiceService.GetShared(IdentityService.CurrentUser);
|
||||
}
|
||||
}
|
263
Moonlight/Shared/Views/Store/Index.razor
Normal file
263
Moonlight/Shared/Views/Store/Index.razor
Normal file
|
@ -0,0 +1,263 @@
|
|||
@page "/store"
|
||||
|
||||
@using Moonlight.App.Database.Entities.Store
|
||||
@using Moonlight.App.Models.Enums
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.Store
|
||||
@using Moonlight.Shared.Components.Store
|
||||
|
||||
@inject Repository<Category> CategoryRepository
|
||||
@inject Repository<Product> ProductRepository
|
||||
@inject ConfigService ConfigService
|
||||
@inject IdentityService IdentityService
|
||||
@inject AlertService AlertService
|
||||
@inject ToastService ToastService
|
||||
@inject StoreService StoreService
|
||||
|
||||
@{
|
||||
var currency = ConfigService.Get().Store.Currency;
|
||||
}
|
||||
|
||||
@if (IdentityService.Permissions[Permission.AdminStore])
|
||||
{
|
||||
<div class="alert alert-info bg-info text-white text-center py-2">
|
||||
@if (EditMode)
|
||||
{
|
||||
<h4 class="pt-2">Edit mode enabled. Disable it by clicking <a href="#" @onclick="ToggleEdit" @onclick:preventDefault>here</a></h4>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h4 class="pt-2">To edit the store you can enable the edit mode <a href="#" @onclick="ToggleEdit" @onclick:preventDefault>here</a></h4>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-12 mb-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="card-title">Categories</h6>
|
||||
@if (EditMode)
|
||||
{
|
||||
<div class="card-toolbar">
|
||||
<button @onclick="() => StoreModals.AddCategoryShow" class="btn btn-icon btn-success">
|
||||
<i class="bx bx-sm bx-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<LazyLoader @ref="CategoriesLazyLoader" Load="LoadCategories">
|
||||
@foreach (var category in Categories)
|
||||
{
|
||||
<div class="d-flex flex-column">
|
||||
<li class="d-flex align-items-center py-2">
|
||||
<span class="bullet me-5"></span>
|
||||
<a class="invisible-a fs-5 @(SelectedCategory == category ? "fw-bold text-primary" : "")" href="/store?category=@(category.Slug)">@(category.Name)</a>
|
||||
@if (EditMode)
|
||||
{
|
||||
<a @onclick="() => StoreModals.EditCategoryShow(category)" @onclick:preventDefault href="#" class="ms-3 text-warning">Edit</a>
|
||||
<a @onclick="() => DeleteCategory(category)" @onclick:preventDefault href="#" class="ms-1 text-danger">Delete</a>
|
||||
}
|
||||
</li>
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9 col-12">
|
||||
<LazyLoader @ref="ProductsLazyLoader" Load="LoadProducts">
|
||||
@if (Products.Any())
|
||||
{
|
||||
<div class="row">
|
||||
@foreach (var product in Products)
|
||||
{
|
||||
<div class="col-md-4 col-12 mb-5">
|
||||
<div class="card">
|
||||
@if (EditMode)
|
||||
{
|
||||
<div class="card-header">
|
||||
<a @onclick="() => StoreModals.EditProductShow(product)" @onclick:preventDefault href="#" class="card-title text-primary">Edit</a>
|
||||
<div class="card-toolbar">
|
||||
<a @onclick="() => DeleteProduct(product)" @onclick:preventDefault href="#" class="text-danger">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="card-body text-center">
|
||||
<h1 class="text-dark mb-5 fw-bolder">@(product.Name)</h1>
|
||||
<p class="fw-semibold fs-6 text-gray-800 flex-grow-1">
|
||||
@(Formatter.FormatLineBreaks(product.Description))
|
||||
</p>
|
||||
|
||||
<div class="text-center mb-8">
|
||||
@if (product.Price == 0)
|
||||
{
|
||||
<span class="fs-1 fw-bold text-primary">
|
||||
Free
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="mb-2 text-primary">@(currency)</span>
|
||||
<span class="fs-1 fw-bold text-primary">
|
||||
@(product.Price)
|
||||
</span>
|
||||
<span class="fs-7 fw-semibold opacity-50">
|
||||
/
|
||||
<span>@(product.Duration) days</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (product.Stock == 0)
|
||||
{
|
||||
<button class="btn btn-primary disabled">Out of stock</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/store/order/@(product.Slug)" class="btn btn-primary">Order now</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (EditMode)
|
||||
{
|
||||
<div class="col-md-4 col-12 mb-5">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<button @onclick="() => StoreModals.AddProductShow" class="btn btn-success">Create new product</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Categories.Any())
|
||||
{
|
||||
if (EditMode)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-body text-center py-10">
|
||||
<button @onclick="() => StoreModals.AddProductShow" class="btn btn-success">Create new product</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card card-body text-center">
|
||||
<div class="py-10">
|
||||
<h1 class="card-title">Welcome to our store</h1>
|
||||
<span class="card-subtitle fs-2">Select a category to start browsing</span>
|
||||
</div>
|
||||
|
||||
<div class="py-10 text-center p-10">
|
||||
<img src="/svg/shopping.svg" style="height: 10vi" alt="Banner">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card card-body text-center">
|
||||
<h1 class="card-title py-10">No products found</h1>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StoreModals @ref="StoreModals" OnUpdate="OnParametersSetAsync" />
|
||||
|
||||
@code
|
||||
{
|
||||
// Category
|
||||
private Category[] Categories;
|
||||
private LazyLoader? CategoriesLazyLoader;
|
||||
|
||||
[Parameter]
|
||||
[SupplyParameterFromQuery]
|
||||
public string Category { get; set; }
|
||||
|
||||
private Category? SelectedCategory;
|
||||
|
||||
// Products
|
||||
private Product[] Products;
|
||||
private LazyLoader? ProductsLazyLoader;
|
||||
|
||||
// Edit stuff
|
||||
private bool EditMode = false;
|
||||
private StoreModals StoreModals;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (CategoriesLazyLoader != null)
|
||||
await CategoriesLazyLoader.Reload();
|
||||
|
||||
if (ProductsLazyLoader != null)
|
||||
await ProductsLazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task ToggleEdit()
|
||||
{
|
||||
EditMode = !EditMode;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Task LoadCategories(LazyLoader _)
|
||||
{
|
||||
Categories = CategoryRepository.Get().ToArray();
|
||||
|
||||
SelectedCategory = Categories.FirstOrDefault(x => x.Slug == Category);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task LoadProducts(LazyLoader _)
|
||||
{
|
||||
if (SelectedCategory == null)
|
||||
{
|
||||
Products = ProductRepository
|
||||
.Get()
|
||||
.Where(x => x.Category == null)
|
||||
.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
Products = ProductRepository
|
||||
.Get()
|
||||
.Where(x => x.Category!.Id == SelectedCategory.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(Category category)
|
||||
{
|
||||
if (!await AlertService.YesNo($"Do you really want to delete '{category.Name}'", "Continue", "Cancel"))
|
||||
return;
|
||||
|
||||
await StoreService.Admin.DeleteCategory(category);
|
||||
|
||||
await ToastService.Success("Successfully deleted category");
|
||||
await OnParametersSetAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteProduct(Product product)
|
||||
{
|
||||
if (!await AlertService.YesNo($"Do you really want to delete '{product.Name}'", "Continue", "Cancel"))
|
||||
return;
|
||||
|
||||
await StoreService.Admin.DeleteProduct(product);
|
||||
|
||||
await ToastService.Success("Successfully deleted product");
|
||||
await OnParametersSetAsync();
|
||||
}
|
||||
}
|
251
Moonlight/Shared/Views/Store/Order.razor
Normal file
251
Moonlight/Shared/Views/Store/Order.razor
Normal file
|
@ -0,0 +1,251 @@
|
|||
@page "/store/order/{slug}"
|
||||
|
||||
@using Moonlight.App.Services.Store
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities.Store
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services.ServiceManage
|
||||
|
||||
@inject ConfigService ConfigService
|
||||
@inject StoreService StoreService
|
||||
@inject IdentityService IdentityService
|
||||
@inject AlertService AlertService
|
||||
@inject NavigationManager Navigation
|
||||
@inject ServiceService ServiceService
|
||||
@inject Repository<Product> ProductRepository
|
||||
@inject Repository<Coupon> CouponRepository
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
@if (SelectedProduct == null)
|
||||
{
|
||||
@*
|
||||
TODO: Add 404 here
|
||||
*@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-7 col-12">
|
||||
<div class="card mb-5">
|
||||
<div class="card-body">
|
||||
<h2 class="fs-2x fw-bold mb-10">@(SelectedProduct.Name)</h2>
|
||||
<p class="text-gray-400 fs-4 fw-semibold">
|
||||
@Formatter.FormatLineBreaks(SelectedProduct.Description)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-4 col-12">
|
||||
<a @onclick="() => SetDurationMultiplier(1)" @onclick:preventDefault href="#" class="card card-body bg-hover-secondary text-center @(DurationMultiplier == 1 ? "border border-info" : "")">
|
||||
<h4 class="fw-bold mb-0 align-middle">@(SelectedProduct.Duration * 1) days</h4>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
<a @onclick="() => SetDurationMultiplier(2)" @onclick:preventDefault href="#" class="card card-body bg-hover-secondary text-center @(DurationMultiplier == 2 ? "border border-info" : "")">
|
||||
<h4 class="fw-bold mb-0 align-middle">@(SelectedProduct.Duration * 2) days</h4>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
<a @onclick="() => SetDurationMultiplier(3)" @onclick:preventDefault href="#" class="card card-body bg-hover-secondary text-center @( DurationMultiplier == 3 ? "border border-info" : "")">
|
||||
<h4 class="fw-bold mb-0 align-middle">@(SelectedProduct.Duration * 3) days</h4>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-5">
|
||||
<div class="card-body">
|
||||
<h3 class="fs-1x fw-bold mb-10">Apply coupon codes</h3>
|
||||
<div class="input-group">
|
||||
<input @bind="CouponCode" class="form-control form-control-solid" placeholder="Coupon code..."/>
|
||||
<button @onclick="ApplyCoupon" class="btn btn-primary">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1 col-12"></div>
|
||||
<div class="col-md-3 col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Summary</h3>
|
||||
</div>
|
||||
|
||||
@{
|
||||
var defaultPrice = SelectedProduct.Price * DurationMultiplier;
|
||||
double actualPrice;
|
||||
|
||||
if (SelectedCoupon == null)
|
||||
actualPrice = defaultPrice;
|
||||
else
|
||||
actualPrice = Math.Round(defaultPrice - (defaultPrice * SelectedCoupon.Percent / 100), 2);
|
||||
|
||||
var currency = ConfigService.Get().Store.Currency;
|
||||
}
|
||||
|
||||
<div class="card-body fs-5">
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Today</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(currency) @(actualPrice)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Renew</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(currency) @(SelectedProduct.Price)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Duration</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(SelectedProduct.Duration * DurationMultiplier) days</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator my-4"></div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Discount</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(SelectedCoupon?.Percent ?? 0)%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Coupon</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(SelectedCoupon?.Code ?? "None")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator my-4"></div>
|
||||
<div class="d-flex flex-stack">
|
||||
<div class="text-gray-700 fw-semibold me-2">Total</div>
|
||||
<div class="d-flex align-items-senter">
|
||||
<span class="fw-bold">@(currency) @(actualPrice)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-7">
|
||||
@if (!CanBeOrdered && !string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
<div class="alert alert-warning bg-warning text-white text-center">
|
||||
@ErrorMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
@if (IsValidating)
|
||||
{
|
||||
<button class="btn btn-primary w-100 disabled" disabled="">
|
||||
<span class="spinner-border spinner-border-sm align-middle"></span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CanBeOrdered)
|
||||
{
|
||||
<WButton OnClick="OnSubmit" Text="@($"Order for {currency} {actualPrice}")" CssClasses="btn btn-primary w-100"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-primary w-100 disabled" disabled="">Order for @(currency) @(actualPrice)</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1 col-12"></div>
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public string Slug { get; set; }
|
||||
|
||||
private Product? SelectedProduct;
|
||||
private Coupon? SelectedCoupon;
|
||||
private int DurationMultiplier = 1;
|
||||
|
||||
private string CouponCode = "";
|
||||
|
||||
private bool CanBeOrdered = false;
|
||||
private bool IsValidating = false;
|
||||
private string ErrorMessage = "";
|
||||
|
||||
private async Task SetDurationMultiplier(int i)
|
||||
{
|
||||
DurationMultiplier = i;
|
||||
|
||||
await Revalidate();
|
||||
}
|
||||
|
||||
private async Task ApplyCoupon()
|
||||
{
|
||||
SelectedCoupon = CouponRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Code == CouponCode);
|
||||
|
||||
CouponCode = "";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (SelectedCoupon == null)
|
||||
{
|
||||
await AlertService.Error("", "Invalid coupon code entered");
|
||||
return;
|
||||
}
|
||||
|
||||
await Revalidate();
|
||||
}
|
||||
|
||||
private Task Revalidate()
|
||||
{
|
||||
if (SelectedProduct == null) // Prevent validating null
|
||||
return Task.CompletedTask;
|
||||
|
||||
IsValidating = true;
|
||||
InvokeAsync(StateHasChanged);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await StoreService.Order.Validate(IdentityService.CurrentUser, SelectedProduct, DurationMultiplier, SelectedCoupon);
|
||||
CanBeOrdered = true;
|
||||
}
|
||||
catch (DisplayException e)
|
||||
{
|
||||
CanBeOrdered = false;
|
||||
ErrorMessage = e.Message;
|
||||
}
|
||||
|
||||
IsValidating = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Load(LazyLoader _)
|
||||
{
|
||||
SelectedProduct = ProductRepository
|
||||
.Get()
|
||||
.FirstOrDefault(x => x.Slug == Slug);
|
||||
|
||||
await Revalidate();
|
||||
}
|
||||
|
||||
private async Task OnSubmit()
|
||||
{
|
||||
if (SelectedProduct == null) // Prevent processing null
|
||||
return;
|
||||
|
||||
// Process the order with the selected values
|
||||
var service = await StoreService
|
||||
.Order
|
||||
.Process(
|
||||
IdentityService.CurrentUser,
|
||||
SelectedProduct,
|
||||
DurationMultiplier,
|
||||
SelectedCoupon
|
||||
);
|
||||
|
||||
Navigation.NavigateTo("/service/" + service.Id);
|
||||
}
|
||||
}
|
1126
Moonlight/wwwroot/css/sweetalert2dark.css
vendored
Normal file
1126
Moonlight/wwwroot/css/sweetalert2dark.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
21
Moonlight/wwwroot/css/utils.css
vendored
Normal file
21
Moonlight/wwwroot/css/utils.css
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
.invisible-a {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.invisible-a:hover {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.blur-unless-hover {
|
||||
filter: blur(5px);
|
||||
}
|
||||
|
||||
.blur-unless-hover:hover {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.blur {
|
||||
filter: blur(5px);
|
||||
}
|
84
Moonlight/wwwroot/js/moonlight.js
vendored
84
Moonlight/wwwroot/js/moonlight.js
vendored
|
@ -21,5 +21,89 @@ window.moonlight = {
|
|||
var toast = new ToastHelper(title, message, color, timeout);
|
||||
toast.show();
|
||||
}
|
||||
},
|
||||
modals: {
|
||||
show: function (id)
|
||||
{
|
||||
let modal = new bootstrap.Modal(document.getElementById(id));
|
||||
modal.show();
|
||||
},
|
||||
hide: function (id)
|
||||
{
|
||||
let element = document.getElementById(id)
|
||||
let modal = bootstrap.Modal.getInstance(element)
|
||||
modal.hide()
|
||||
}
|
||||
},
|
||||
alerts: {
|
||||
getHelper: function(){
|
||||
return Swal.mixin({
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-success',
|
||||
cancelButton: 'btn btn-danger',
|
||||
denyButton: 'btn btn-danger',
|
||||
htmlContainer: 'text-white'
|
||||
},
|
||||
buttonsStyling: false
|
||||
});
|
||||
},
|
||||
info: function (title, description) {
|
||||
this.getHelper().fire(
|
||||
title,
|
||||
description,
|
||||
'info'
|
||||
)
|
||||
},
|
||||
success: function (title, description) {
|
||||
this.getHelper().fire(
|
||||
title,
|
||||
description,
|
||||
'success'
|
||||
)
|
||||
},
|
||||
warning: function (title, description) {
|
||||
this.getHelper().fire(
|
||||
title,
|
||||
description,
|
||||
'warning'
|
||||
)
|
||||
},
|
||||
error: function (title, description) {
|
||||
this.getHelper().fire(
|
||||
title,
|
||||
description,
|
||||
'error'
|
||||
)
|
||||
},
|
||||
yesno: function (title, yesText, noText) {
|
||||
return this.getHelper().fire({
|
||||
title: title,
|
||||
showDenyButton: true,
|
||||
confirmButtonText: yesText,
|
||||
denyButtonText: noText,
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
return true;
|
||||
} else if (result.isDenied) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
},
|
||||
text: function (title, description) {
|
||||
const {value: text} = this.getHelper().fire({
|
||||
title: title,
|
||||
input: 'text',
|
||||
inputLabel: description,
|
||||
inputValue: "",
|
||||
showCancelButton: false,
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return 'You need to enter a value'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
6
Moonlight/wwwroot/js/sweetalert2.js
vendored
Normal file
6
Moonlight/wwwroot/js/sweetalert2.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Moonlight/wwwroot/svg/shopping.svg
vendored
Normal file
1
Moonlight/wwwroot/svg/shopping.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
Loading…
Reference in a new issue