212 lines
6.1 KiB
C#

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Web;
using Microsoft.IdentityModel.Tokens;
using OnlyPrompt.Backend.Database;
using OnlyPrompt.Backend.Database.Models;
using OnlyPrompt.Backend.Services.Jwt;
using OnlyPrompt.Backend.Utils;
using Scalar.AspNetCore;
using System.Text;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
builder.Services.AddDbContext<OnlyPromptContext>(opts =>
{
opts.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"));
opts.UseLazyLoadingProxies();
opts.ConfigureWarnings(warnings =>
warnings.Ignore(RelationalEventId.PendingModelChangesWarning));
});
builder.Services.AddSingleton<IPasswordHasher<UserModel>, PasswordHasher<UserModel>>();
builder.Services.AddSingleton<ITokenService, JwtTokenService>();
builder.Services.AddAutoMapper(AutoMapperSetup.Setup);
builder.Services.AddValidation(opts =>
{
opts.MaxDepth = 10;
});
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, opts =>
{
opts.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = config["Jwt:Issuer"],
ValidAudience = config["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]))
};
opts.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
if (context.Request.Cookies.ContainsKey("jwt"))
context.Token = context.Request.Cookies["jwt"];
return Task.CompletedTask;
}
};
});
builder.Services.AddControllers().AddJsonOptions(jsonOpts =>
{
jsonOpts.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
builder.Services.AddOpenApi(opts => opts.AddScalarTransformers());
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
var rewrite = new RewriteOptions()
.AddRewrite(@"^(?!scalar\/?|api\/?)([^.]+)$", "$1.html", skipRemainingRules: true);
app.UseRewriter(rewrite);
app.UseAuthentication();
app.UseAuthorization();
if (app.Environment.IsDevelopment())
{
var dir = Path.GetFullPath("./../OnlyPrompt.Frontend");
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(dir),
RedirectToAppendTrailingSlash = true,
HttpsCompression = HttpsCompressionMode.Compress,
});
}
else
{
app.UseStaticFiles();
}
app.MapControllers();
app.MapGet("/", async (HttpContext context) =>
{
var authResult = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
return authResult.Succeeded
? Results.Redirect("/dashboard")
: Results.Redirect("/login");
});
app.MapFallbackToFile("/login.html");
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<OnlyPromptContext>();
await db.Database.MigrateAsync();
await EnsurePromptDetailColumnsAsync(db);
await EnsurePromptLikesTableAsync(db);
await EnsurePromptSavesTableAsync(db);
await SeedDefaultCategoriesAsync(db);
app.Run();
static async Task EnsurePromptDetailColumnsAsync(OnlyPromptContext db)
{
await db.Database.ExecuteSqlRawAsync("""
ALTER TABLE "Prompts"
ADD COLUMN IF NOT EXISTS "ExampleImageUrl" text;
""");
await db.Database.ExecuteSqlRawAsync("""
ALTER TABLE "Prompts"
ADD COLUMN IF NOT EXISTS "ExampleOutput" text;
""");
await db.Database.ExecuteSqlRawAsync("""
ALTER TABLE "Prompts"
ADD COLUMN IF NOT EXISTS "Price" numeric;
""");
await db.Database.ExecuteSqlRawAsync("""
ALTER TABLE "Prompts"
ALTER COLUMN "Prompt" TYPE text;
""");
await db.Database.ExecuteSqlRawAsync("""
ALTER TABLE "Prompts"
ALTER COLUMN "ExampleOutput" TYPE text;
""");
}
static async Task EnsurePromptLikesTableAsync(OnlyPromptContext db)
{
await db.Database.ExecuteSqlRawAsync("""
CREATE TABLE IF NOT EXISTS "PromptLikes" (
"PromptId" uuid NOT NULL,
"UserId" uuid NOT NULL,
CONSTRAINT "PK_PromptLikes" PRIMARY KEY ("PromptId", "UserId"),
CONSTRAINT "FK_PromptLikes_Prompts_PromptId" FOREIGN KEY ("PromptId") REFERENCES "Prompts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PromptLikes_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users" ("Id") ON DELETE CASCADE
);
""");
await db.Database.ExecuteSqlRawAsync("""
CREATE INDEX IF NOT EXISTS "IX_PromptLikes_UserId" ON "PromptLikes" ("UserId");
""");
}
static async Task EnsurePromptSavesTableAsync(OnlyPromptContext db)
{
await db.Database.ExecuteSqlRawAsync("""
CREATE TABLE IF NOT EXISTS "PromptSaves" (
"PromptId" uuid NOT NULL,
"UserId" uuid NOT NULL,
CONSTRAINT "PK_PromptSaves" PRIMARY KEY ("PromptId", "UserId"),
CONSTRAINT "FK_PromptSaves_Prompts_PromptId" FOREIGN KEY ("PromptId") REFERENCES "Prompts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PromptSaves_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users" ("Id") ON DELETE CASCADE
);
""");
await db.Database.ExecuteSqlRawAsync("""
CREATE INDEX IF NOT EXISTS "IX_PromptSaves_UserId" ON "PromptSaves" ("UserId");
""");
}
static async Task SeedDefaultCategoriesAsync(OnlyPromptContext db)
{
var defaults = new[]
{
("creative-writing", "Creative Writing"),
("coding", "Coding"),
("art", "Art"),
("marketing", "Marketing"),
("video", "Video"),
("data", "Data")
};
foreach (var (slug, name) in defaults)
{
if (await db.Categories.AnyAsync(c => c.Slug == slug))
continue;
db.Categories.Add(new CategoryModel
{
Id = Guid.CreateVersion7(),
Slug = slug,
Name = name,
Description = $"{name} prompts"
});
}
await db.SaveChangesAsync();
}