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(opts => { opts.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")); opts.UseLazyLoadingProxies(); opts.ConfigureWarnings(warnings => warnings.Ignore(RelationalEventId.PendingModelChangesWarning)); }); builder.Services.AddSingleton, PasswordHasher>(); builder.Services.AddSingleton(); 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(); 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(); }