- started implementing backend
30
.dockerignore
Normal file
@ -0,0 +1,30 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
!**/.gitignore
|
||||
!.git/HEAD
|
||||
!.git/config
|
||||
!.git/packed-refs
|
||||
!.git/refs/heads/**
|
||||
428
.gitignore
vendored
Normal file
@ -0,0 +1,428 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.env
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
|
||||
[Dd]ebug/x64/
|
||||
[Dd]ebugPublic/x64/
|
||||
[Rr]elease/x64/
|
||||
[Rr]eleases/x64/
|
||||
bin/x64/
|
||||
obj/x64/
|
||||
|
||||
[Dd]ebug/x86/
|
||||
[Dd]ebugPublic/x86/
|
||||
[Rr]elease/x86/
|
||||
[Rr]eleases/x86/
|
||||
bin/x86/
|
||||
obj/x86/
|
||||
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
[Aa][Rr][Mm]64[Ee][Cc]/
|
||||
bld/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Build results on 'Bin' directories
|
||||
**/[Bb]in/*
|
||||
# Uncomment if you have tasks that rely on *.refresh files to move binaries
|
||||
# (https://github.com/github/gitignore/pull/3736)
|
||||
#!**/[Bb]in/*.refresh
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
*.trx
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Approval Tests result files
|
||||
*.received.*
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.idb
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
**/.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
**/.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
**/.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
#tools/**
|
||||
#!tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
MSBuild_Logs/
|
||||
|
||||
# AWS SAM Build and Temporary Artifacts folder
|
||||
.aws-sam
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
**/.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
**/.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
**/.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
4
OnlyPrompt.Backend/ApiModels/Auth/Models.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace OnlyPrompt.Backend.ApiModels.Auth
|
||||
{
|
||||
public record ApiUser(Guid Id, string UserName, string Email, string[] Roles);
|
||||
}
|
||||
8
OnlyPrompt.Backend/ApiModels/Auth/Requests.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using OnlyPrompt.Backend.ApiModels.Validators;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace OnlyPrompt.Backend.ApiModels.Auth
|
||||
{
|
||||
public record ApiLoginRequest(string UserNameOrEmail, string Password);
|
||||
public record ApiRegisterRequest([MaxLength(100)] string DisplayName, [MaxLength(100)][NoWhitespace] string UserName, string Email, string Password);
|
||||
}
|
||||
4
OnlyPrompt.Backend/ApiModels/UserProfile/Models.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace OnlyPrompt.Backend.ApiModels.UserProfile
|
||||
{
|
||||
public record ApiUserProfile(string DisplayName, string Slug, string? Bio, string AvatarUrl, string? Specialities, double AverageRating, int Subscribers);
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
|
||||
namespace OnlyPrompt.Backend.ApiModels.Validators
|
||||
{
|
||||
public class NoWhitespaceAttribute : ValidationAttribute
|
||||
{
|
||||
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
|
||||
{
|
||||
if(value is string strValue)
|
||||
{
|
||||
if(strValue.Any(c => char.IsWhiteSpace(c)))
|
||||
return new ValidationResult($"{validationContext.DisplayName} should not contain any whitespace characters.");
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
return base.IsValid(value, validationContext);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
OnlyPrompt.Backend/Controllers/AdminController.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using OnlyPrompt.Backend.Database;
|
||||
|
||||
namespace OnlyPrompt.Backend.Controllers
|
||||
{
|
||||
public class AdminController : BaseController
|
||||
{
|
||||
public AdminController(OnlyPromptContext db) : base(db)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
105
OnlyPrompt.Backend/Controllers/AuthController.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.ApiModels.Auth;
|
||||
using OnlyPrompt.Backend.Database;
|
||||
using OnlyPrompt.Backend.Database.Models;
|
||||
using OnlyPrompt.Backend.Services.Jwt;
|
||||
using OnlyPrompt.Backend.Utils;
|
||||
|
||||
namespace OnlyPrompt.Backend.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/v1/auth")]
|
||||
public class AuthController : BaseController
|
||||
{
|
||||
private static readonly CookieOptions AuthCookieOptions = new CookieOptions { Secure = true, HttpOnly = true, IsEssential = true };
|
||||
private readonly IPasswordHasher<UserModel> _passwordHasher;
|
||||
private readonly ITokenService _jwtService;
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public AuthController(OnlyPromptContext db, IPasswordHasher<UserModel> passwordHasher, IMapper mapper, ILogger<AuthController> logger, ITokenService jwtService) : base(db)
|
||||
{
|
||||
_passwordHasher=passwordHasher;
|
||||
_mapper=mapper;
|
||||
_logger=logger;
|
||||
_jwtService=jwtService;
|
||||
}
|
||||
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("login")]
|
||||
public async Task<Results<RedirectHttpResult, BadRequest<string>, NotFound<string>>> LoginAsync([FromBody] ApiLoginRequest request)
|
||||
{
|
||||
var user = await FindUserAsync(request.UserNameOrEmail);
|
||||
if (user is null)
|
||||
return TypedResults.NotFound("User not found");
|
||||
|
||||
var verificationResult = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password);
|
||||
if (verificationResult == PasswordVerificationResult.Failed)
|
||||
return TypedResults.NotFound("User not found"); // Don't reveal that the user exists
|
||||
|
||||
if (user.IsLockoutEnabled)
|
||||
return TypedResults.BadRequest("User is locked out"); // Don't reveal that the user exists
|
||||
|
||||
var token = _jwtService.BuildToken(user, out var validUntil);
|
||||
this.Response.Cookies.Append("jwt", token, AuthCookieOptions.Copy(c => c.Expires = validUntil));
|
||||
return TypedResults.Redirect("feed");
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("register")]
|
||||
public async Task<Results<RedirectHttpResult, ValidationProblem, Ok<ApiUser>>> RegisterAsync([FromBody] ApiRegisterRequest request)
|
||||
{
|
||||
var existingUser = await FindUserAsync(request.UserName, request.Email);
|
||||
if (existingUser is not null)
|
||||
{
|
||||
var errors = new Dictionary<string, string[]>();
|
||||
|
||||
if (existingUser.UserName == request.UserName)
|
||||
errors.Add(nameof(request.UserName), ["Username is already taken"]);
|
||||
|
||||
if (existingUser.Email == request.Email)
|
||||
errors.Add(nameof(request.Email), ["Email is already registered"]);
|
||||
|
||||
return TypedResults.ValidationProblem(errors);
|
||||
}
|
||||
|
||||
var id = Guid.NewGuid();
|
||||
var slug = await SlugHelper.GenerateUniqueSlug(request.UserName, s => _db.UserProfiles.AnyAsync(up => up.Slug == s), 32);
|
||||
var avatarUrl = $"https://api.dicebear.com/9.x/bottts/svg?seed={id}";
|
||||
var newUser = new UserModel
|
||||
{
|
||||
Id = id,
|
||||
Profile = new UserProfileModel
|
||||
{
|
||||
AvatarUrl = avatarUrl,
|
||||
DisplayName = request.DisplayName,
|
||||
Slug = slug,
|
||||
},
|
||||
Roles = [ModelConstants.UserRole],
|
||||
PasswordHash = null,
|
||||
UserName = request.UserName,
|
||||
Email = request.Email,
|
||||
IsLockoutEnabled = false,
|
||||
};
|
||||
|
||||
newUser.PasswordHash = _passwordHasher.HashPassword(newUser, request.Password);
|
||||
_db.Users.Add(newUser);
|
||||
await _db.SaveChangesAsync();
|
||||
return TypedResults.Ok(_mapper.Map<ApiUser>(newUser));
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("logout")]
|
||||
public RedirectHttpResult Logout()
|
||||
{
|
||||
this.Response.Cookies.Delete("jwt", AuthCookieOptions);
|
||||
return TypedResults.Redirect("login");
|
||||
}
|
||||
}
|
||||
}
|
||||
35
OnlyPrompt.Backend/Controllers/BaseController.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.Database;
|
||||
using OnlyPrompt.Backend.Database.Models;
|
||||
using OnlyPrompt.Backend.Utils;
|
||||
|
||||
namespace OnlyPrompt.Backend.Controllers
|
||||
{
|
||||
public abstract class BaseController : Controller
|
||||
{
|
||||
protected OnlyPromptContext _db;
|
||||
protected IMapper _mapper;
|
||||
|
||||
public BaseController(OnlyPromptContext db, IMapper mapper)
|
||||
{
|
||||
_db=db;
|
||||
_mapper=mapper;
|
||||
}
|
||||
|
||||
public Task<UserModel?> FindUserAsync(Guid id) => _db.Users.FirstOrDefaultAsync(x => x.Id == id);
|
||||
public Task<UserModel?> FindUserAsync(string userName, string email) => _db.Users.FirstOrDefaultAsync(x => x.Email == email || x.UserName == userName);
|
||||
public Task<UserModel?> FindUserAsync(string emailOrUsername) => _db.Users.FirstOrDefaultAsync(x => x.Email == emailOrUsername || x.UserName == emailOrUsername);
|
||||
|
||||
public async Task<UserModel?> GetUserAsync()
|
||||
{
|
||||
var id = User.GetUserId();
|
||||
if (id.HasValue == false)
|
||||
return null;
|
||||
|
||||
var user = await _db.Users.FindAsync(id.Value);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
OnlyPrompt.Backend/Controllers/ProfileController.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.ApiModels.UserProfile;
|
||||
using OnlyPrompt.Backend.Database;
|
||||
using OnlyPrompt.Backend.Utils;
|
||||
|
||||
namespace OnlyPrompt.Backend.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/v1/profiles")]
|
||||
[Authorize(Roles = ModelConstants.UserRole)]
|
||||
public class ProfileController : BaseController
|
||||
{
|
||||
public ProfileController(OnlyPromptContext db, IMapper mapper) : base(db, mapper)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<Results<NotFound<string>, Ok<ApiUserProfile>>> GetProfileAsync(Identifier id)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var profile = await _db.UserProfiles.OfIdentifer(id)
|
||||
.Where(up => up.IsPublic || up.Id == userId)
|
||||
.ProjectTo<ApiUserProfile>(_mapper.ConfigurationProvider)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (profile is null)
|
||||
return TypedResults.NotFound("Profile not found or is private.");
|
||||
|
||||
return TypedResults.Ok(profile);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
15
OnlyPrompt.Backend/Database/Core/EntityBase.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OnlyPrompt.Backend.Database.Core
|
||||
{
|
||||
public class EntityBase : IEntity
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
7
OnlyPrompt.Backend/Database/Core/IEntity.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace OnlyPrompt.Backend.Database.Core
|
||||
{
|
||||
public interface IEntity
|
||||
{
|
||||
public Guid Id { get; }
|
||||
}
|
||||
}
|
||||
7
OnlyPrompt.Backend/Database/Core/IHasSlug.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace OnlyPrompt.Backend.Database.Core
|
||||
{
|
||||
public interface IHasSlug
|
||||
{
|
||||
public string Slug { get; set; }
|
||||
}
|
||||
}
|
||||
8
OnlyPrompt.Backend/Database/ModelConstants.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace OnlyPrompt.Backend.Database
|
||||
{
|
||||
public static class ModelConstants
|
||||
{
|
||||
public const int MaxSlugLength = 100;
|
||||
public const string UserRole = "user";
|
||||
}
|
||||
}
|
||||
19
OnlyPrompt.Backend/Database/Models/CategoryModel.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.Database.Core;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OnlyPrompt.Backend.Database.Models
|
||||
{
|
||||
[Index(nameof(Slug), IsUnique = true)]
|
||||
public class CategoryModel : EntityBase, IHasSlug
|
||||
{
|
||||
[MaxLength(ModelConstants.MaxSlugLength)]
|
||||
[Column(TypeName = "citext")]
|
||||
public required string Slug { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
|
||||
public virtual IList<PromptModel> Prompts { get; set; } = new List<PromptModel>();
|
||||
}
|
||||
}
|
||||
41
OnlyPrompt.Backend/Database/Models/PromptModel.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.Database.Core;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OnlyPrompt.Backend.Database.Models
|
||||
{
|
||||
[Index(nameof(Slug), IsUnique = true)]
|
||||
public class PromptModel : EntityBase, IHasSlug
|
||||
{
|
||||
[Required]
|
||||
[ForeignKey(nameof(Creator))]
|
||||
public Guid CreatorId { get; set; }
|
||||
|
||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
||||
public required virtual UserModel Creator { get; set; }
|
||||
|
||||
[Required]
|
||||
[ForeignKey(nameof(Category))]
|
||||
public Guid CategoryId { get; set; }
|
||||
|
||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
||||
public required virtual CategoryModel Category { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public required string Title { get; set; }
|
||||
public required string Content { get; set; }
|
||||
|
||||
|
||||
[MaxLength(ModelConstants.MaxSlugLength)]
|
||||
[Column(TypeName = "citext")]
|
||||
public required string Slug { get; set; }
|
||||
|
||||
[ForeignKey(nameof(SubscriptionTier))]
|
||||
public Guid? SubscriptionTierId { get; set; }
|
||||
|
||||
[DeleteBehavior(DeleteBehavior.SetNull)]
|
||||
public virtual SubscriptionTierModel? SubscriptionTier { get; set; }
|
||||
public virtual IList<ReviewModel> Reviews { get; set; } = new List<ReviewModel>();
|
||||
}
|
||||
}
|
||||
31
OnlyPrompt.Backend/Database/Models/ReviewModel.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.Database.Core;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OnlyPrompt.Backend.Database.Models
|
||||
{
|
||||
[Index(nameof(ReviewerId), nameof(PromptId), IsUnique = true)]
|
||||
public class ReviewModel : EntityBase
|
||||
{
|
||||
[Required]
|
||||
[ForeignKey(nameof(Reviewer))]
|
||||
public Guid ReviewerId { get; set; }
|
||||
|
||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
||||
public virtual UserModel Reviewer { get; set; }
|
||||
|
||||
[Required]
|
||||
[ForeignKey(nameof(Prompt))]
|
||||
public Guid PromptId { get; set; }
|
||||
|
||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
||||
public virtual PromptModel Prompt { get; set; }
|
||||
|
||||
[Range(1, 5)]
|
||||
public int Rating { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? Comment { get; set; } = null;
|
||||
}
|
||||
}
|
||||
32
OnlyPrompt.Backend/Database/Models/SubscriptionModel.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OnlyPrompt.Backend.Database.Models
|
||||
{
|
||||
[Table("UserSubscriptions")]
|
||||
[PrimaryKey(nameof(SubscriberId), nameof(SubscribedToId))]
|
||||
public class SubscriptionModel
|
||||
{
|
||||
[Required]
|
||||
[ForeignKey(nameof(SubscribedTo))]
|
||||
public Guid SubscribedToId { get; set; }
|
||||
|
||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
||||
public required virtual UserModel SubscribedTo { get; set; }
|
||||
|
||||
[Required]
|
||||
[ForeignKey(nameof(Subscriber))]
|
||||
public Guid SubscriberId { get; set; }
|
||||
|
||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
||||
public required virtual UserModel Subscriber { get; set; }
|
||||
|
||||
|
||||
[ForeignKey(nameof(SubscriptionTier))]
|
||||
public virtual Guid? SubscriptionTierId { get; set; }
|
||||
|
||||
[DeleteBehavior(DeleteBehavior.SetNull)]
|
||||
public virtual SubscriptionTierModel? SubscriptionTier { get; set; }
|
||||
}
|
||||
}
|
||||
30
OnlyPrompt.Backend/Database/Models/SubscriptionTierModel.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.Database.Core;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OnlyPrompt.Backend.Database.Models
|
||||
{
|
||||
[Index(nameof(Level), nameof(UserId), IsUnique = true)]
|
||||
public class SubscriptionTierModel : EntityBase
|
||||
{
|
||||
[Required]
|
||||
public required string Name { get; set; }
|
||||
|
||||
[MaxLength(1000)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[Required]
|
||||
[ForeignKey(nameof(User))]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
[DeleteBehavior(DeleteBehavior.Cascade)]
|
||||
public required virtual UserModel User { get; set; }
|
||||
|
||||
public decimal MonthlyPrice { get; set; }
|
||||
public int Level { get; set; }
|
||||
|
||||
public virtual IList<PromptModel> Prompts { get; set; } = new List<PromptModel>();
|
||||
public virtual IList<SubscriptionModel> Subscriptions { get; set; } = new List<SubscriptionModel>();
|
||||
}
|
||||
}
|
||||
30
OnlyPrompt.Backend/Database/Models/UserModel.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.Database.Core;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OnlyPrompt.Backend.Database.Models
|
||||
{
|
||||
[Index(nameof(Email), IsUnique = true)]
|
||||
[Index(nameof(UserName), IsUnique = true)]
|
||||
public class UserModel : EntityBase
|
||||
{
|
||||
[MaxLength(100)]
|
||||
[Column(TypeName = "citext")]
|
||||
public required string UserName { get; set; }
|
||||
|
||||
[Column(TypeName = "citext")]
|
||||
public required string Email { get; set; }
|
||||
public required string PasswordHash { get; set; }
|
||||
public required string[] Roles { get; set; }
|
||||
|
||||
[Required]
|
||||
public required virtual UserProfileModel Profile { get; set; }
|
||||
|
||||
public virtual IList<PromptModel> Prompts { get; set; } = new List<PromptModel>();
|
||||
public virtual IList<SubscriptionModel> Subscriptions { get; set; } = new List<SubscriptionModel>();
|
||||
public virtual IList<SubscriptionModel> Subscribers { get; set; } = new List<SubscriptionModel>();
|
||||
|
||||
public bool IsLockoutEnabled { get; set; } = false;
|
||||
}
|
||||
}
|
||||
29
OnlyPrompt.Backend/Database/Models/UserProfileModel.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.Database.Core;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OnlyPrompt.Backend.Database.Models
|
||||
{
|
||||
[Index(nameof(Slug), IsUnique = true)]
|
||||
public class UserProfileModel : EntityBase, IHasSlug
|
||||
{
|
||||
[MaxLength(100)]
|
||||
public required string DisplayName { get; set; }
|
||||
|
||||
[MaxLength(ModelConstants.MaxSlugLength)]
|
||||
[Column(TypeName = "citext")]
|
||||
public required string Slug { get; set; }
|
||||
|
||||
[MaxLength(2000)]
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public required string AvatarUrl { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? Specialities { get; set; }
|
||||
|
||||
public virtual UserModel User { get; set; }
|
||||
public bool IsPublic { get; set; } = false;
|
||||
}
|
||||
}
|
||||
56
OnlyPrompt.Backend/Database/OnlyPromptContext.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using OnlyPrompt.Backend.Database.Models;
|
||||
|
||||
namespace OnlyPrompt.Backend.Database
|
||||
{
|
||||
public class OnlyPromptContext : DbContext
|
||||
{
|
||||
public DbSet<UserModel> Users { get; set; }
|
||||
public DbSet<UserProfileModel> UserProfiles { get; set; }
|
||||
public DbSet<CategoryModel> Categories { get; set; }
|
||||
public DbSet<PromptModel> Prompts { get; set; }
|
||||
public DbSet<SubscriptionTierModel> SubscriptionTiers { get; set; }
|
||||
public DbSet<SubscriptionModel> Subscriptions { get; set; }
|
||||
public DbSet<ReviewModel> Reviews { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
if(optionsBuilder.IsConfigured == false)
|
||||
{
|
||||
optionsBuilder.UseNpgsql();
|
||||
optionsBuilder.UseLazyLoadingProxies();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.Entity<UserModel>(entity =>
|
||||
{
|
||||
entity.HasOne(e => e.Profile)
|
||||
.WithOne(p => p.User)
|
||||
.HasForeignKey<UserProfileModel>(p => p.Id);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<SubscriptionModel>(entity =>
|
||||
{
|
||||
entity.HasOne(e => e.Subscriber)
|
||||
.WithMany(s => s.Subscriptions)
|
||||
.HasForeignKey(e => e.SubscriberId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(e => e.SubscribedTo)
|
||||
.WithMany(c => c.Subscribers)
|
||||
.HasForeignKey(e => e.SubscribedToId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(e => e.SubscriptionTier)
|
||||
.WithMany(t => t.Subscriptions)
|
||||
.HasForeignKey(e => e.SubscriptionTierId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
30
OnlyPrompt.Backend/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
# This stage is used when running from VS in fast mode (Default for Debug configuration)
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
|
||||
# This stage is used to build the service project
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["OnlyPrompt.Backend/OnlyPrompt.Backend.csproj", "OnlyPrompt.Backend/"]
|
||||
RUN dotnet restore "./OnlyPrompt.Backend/OnlyPrompt.Backend.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/OnlyPrompt.Backend"
|
||||
RUN dotnet build "./OnlyPrompt.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# This stage is used to publish the service project to be copied to the final stage
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./OnlyPrompt.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "OnlyPrompt.Backend.dll"]
|
||||
419
OnlyPrompt.Backend/Migrations/20260411191205_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,419 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using OnlyPrompt.Backend.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OnlyPrompt.Backend.Migrations
|
||||
{
|
||||
[DbContext(typeof(OnlyPromptContext))]
|
||||
[Migration("20260411191205_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.5")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true)
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Categories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.Property<Guid?>("SubscriptionTierId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("CreatorId");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SubscriptionTierId");
|
||||
|
||||
b.ToTable("Prompts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("PromptId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Rating")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("ReviewerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PromptId");
|
||||
|
||||
b.HasIndex("ReviewerId", "PromptId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Reviews");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
||||
{
|
||||
b.Property<Guid>("SubscriberId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SubscribedToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("SubscriptionTierId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SubscriberId", "SubscribedToId");
|
||||
|
||||
b.HasIndex("SubscribedToId");
|
||||
|
||||
b.HasIndex("SubscriptionTierId");
|
||||
|
||||
b.ToTable("UserSubscriptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("MonthlyPrice")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Level", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SubscriptionTiers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.Property<bool>("IsLockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Roles")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AvatarUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<bool>("IsPublic")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.Property<string>("Specialities")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserProfiles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.CategoryModel", "Category")
|
||||
.WithMany("Prompts")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Creator")
|
||||
.WithMany("Prompts")
|
||||
.HasForeignKey("CreatorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
||||
.WithMany("Prompts")
|
||||
.HasForeignKey("SubscriptionTierId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Creator");
|
||||
|
||||
b.Navigation("SubscriptionTier");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.PromptModel", "Prompt")
|
||||
.WithMany("Reviews")
|
||||
.HasForeignKey("PromptId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Reviewer")
|
||||
.WithMany()
|
||||
.HasForeignKey("ReviewerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Prompt");
|
||||
|
||||
b.Navigation("Reviewer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "SubscribedTo")
|
||||
.WithMany("Subscribers")
|
||||
.HasForeignKey("SubscribedToId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Subscriber")
|
||||
.WithMany("Subscriptions")
|
||||
.HasForeignKey("SubscriberId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
||||
.WithMany("Subscriptions")
|
||||
.HasForeignKey("SubscriptionTierId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("SubscribedTo");
|
||||
|
||||
b.Navigation("Subscriber");
|
||||
|
||||
b.Navigation("SubscriptionTier");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
||||
.WithOne("Profile")
|
||||
.HasForeignKey("OnlyPrompt.Backend.Database.Models.UserProfileModel", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
||||
{
|
||||
b.Navigation("Prompts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
||||
{
|
||||
b.Navigation("Reviews");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
||||
{
|
||||
b.Navigation("Prompts");
|
||||
|
||||
b.Navigation("Subscriptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
||||
{
|
||||
b.Navigation("Profile")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Prompts");
|
||||
|
||||
b.Navigation("Subscribers");
|
||||
|
||||
b.Navigation("Subscriptions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
300
OnlyPrompt.Backend/Migrations/20260411191205_Initial.cs
Normal file
@ -0,0 +1,300 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OnlyPrompt.Backend.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:citext", ",,");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Slug = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Description = table.Column<string>(type: "text", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserName = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
||||
Email = table.Column<string>(type: "citext", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: false),
|
||||
Roles = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
IsLockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SubscriptionTiers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
MonthlyPrice = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
Level = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SubscriptionTiers", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SubscriptionTiers_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserProfiles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Slug = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
||||
Bio = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||
AvatarUrl = table.Column<string>(type: "text", nullable: false),
|
||||
Specialities = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
IsPublic = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserProfiles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserProfiles_Users_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Prompts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CategoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Content = table.Column<string>(type: "text", nullable: false),
|
||||
Slug = table.Column<string>(type: "citext", maxLength: 100, nullable: false),
|
||||
SubscriptionTierId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Prompts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Prompts_Categories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "Categories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Prompts_SubscriptionTiers_SubscriptionTierId",
|
||||
column: x => x.SubscriptionTierId,
|
||||
principalTable: "SubscriptionTiers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_Prompts_Users_CreatorId",
|
||||
column: x => x.CreatorId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserSubscriptions",
|
||||
columns: table => new
|
||||
{
|
||||
SubscribedToId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SubscriberId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SubscriptionTierId = table.Column<Guid>(type: "uuid", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserSubscriptions", x => new { x.SubscriberId, x.SubscribedToId });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserSubscriptions_SubscriptionTiers_SubscriptionTierId",
|
||||
column: x => x.SubscriptionTierId,
|
||||
principalTable: "SubscriptionTiers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserSubscriptions_Users_SubscribedToId",
|
||||
column: x => x.SubscribedToId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserSubscriptions_Users_SubscriberId",
|
||||
column: x => x.SubscriberId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Reviews",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ReviewerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PromptId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Rating = table.Column<int>(type: "integer", nullable: false),
|
||||
Comment = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Reviews", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Reviews_Prompts_PromptId",
|
||||
column: x => x.PromptId,
|
||||
principalTable: "Prompts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Reviews_Users_ReviewerId",
|
||||
column: x => x.ReviewerId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_Slug",
|
||||
table: "Categories",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Prompts_CategoryId",
|
||||
table: "Prompts",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Prompts_CreatorId",
|
||||
table: "Prompts",
|
||||
column: "CreatorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Prompts_Slug",
|
||||
table: "Prompts",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Prompts_SubscriptionTierId",
|
||||
table: "Prompts",
|
||||
column: "SubscriptionTierId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Reviews_PromptId",
|
||||
table: "Reviews",
|
||||
column: "PromptId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Reviews_ReviewerId_PromptId",
|
||||
table: "Reviews",
|
||||
columns: new[] { "ReviewerId", "PromptId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SubscriptionTiers_Level_UserId",
|
||||
table: "SubscriptionTiers",
|
||||
columns: new[] { "Level", "UserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SubscriptionTiers_UserId",
|
||||
table: "SubscriptionTiers",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserProfiles_Slug",
|
||||
table: "UserProfiles",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_Email",
|
||||
table: "Users",
|
||||
column: "Email",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_UserName",
|
||||
table: "Users",
|
||||
column: "UserName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserSubscriptions_SubscribedToId",
|
||||
table: "UserSubscriptions",
|
||||
column: "SubscribedToId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserSubscriptions_SubscriptionTierId",
|
||||
table: "UserSubscriptions",
|
||||
column: "SubscriptionTierId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Reviews");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserProfiles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserSubscriptions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Prompts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SubscriptionTiers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
416
OnlyPrompt.Backend/Migrations/OnlyPromptContextModelSnapshot.cs
Normal file
@ -0,0 +1,416 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using OnlyPrompt.Backend.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OnlyPrompt.Backend.Migrations
|
||||
{
|
||||
[DbContext(typeof(OnlyPromptContext))]
|
||||
partial class OnlyPromptContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.5")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true)
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Categories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.Property<Guid?>("SubscriptionTierId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("CreatorId");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SubscriptionTierId");
|
||||
|
||||
b.ToTable("Prompts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("PromptId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Rating")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("ReviewerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PromptId");
|
||||
|
||||
b.HasIndex("ReviewerId", "PromptId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Reviews");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
||||
{
|
||||
b.Property<Guid>("SubscriberId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SubscribedToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("SubscriptionTierId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SubscriberId", "SubscribedToId");
|
||||
|
||||
b.HasIndex("SubscribedToId");
|
||||
|
||||
b.HasIndex("SubscriptionTierId");
|
||||
|
||||
b.ToTable("UserSubscriptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("MonthlyPrice")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("Level", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SubscriptionTiers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.Property<bool>("IsLockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Roles")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AvatarUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<bool>("IsPublic")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("citext");
|
||||
|
||||
b.Property<string>("Specialities")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserProfiles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.CategoryModel", "Category")
|
||||
.WithMany("Prompts")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Creator")
|
||||
.WithMany("Prompts")
|
||||
.HasForeignKey("CreatorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
||||
.WithMany("Prompts")
|
||||
.HasForeignKey("SubscriptionTierId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Creator");
|
||||
|
||||
b.Navigation("SubscriptionTier");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.ReviewModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.PromptModel", "Prompt")
|
||||
.WithMany("Reviews")
|
||||
.HasForeignKey("PromptId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Reviewer")
|
||||
.WithMany()
|
||||
.HasForeignKey("ReviewerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Prompt");
|
||||
|
||||
b.Navigation("Reviewer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "SubscribedTo")
|
||||
.WithMany("Subscribers")
|
||||
.HasForeignKey("SubscribedToId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "Subscriber")
|
||||
.WithMany("Subscriptions")
|
||||
.HasForeignKey("SubscriberId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", "SubscriptionTier")
|
||||
.WithMany("Subscriptions")
|
||||
.HasForeignKey("SubscriptionTierId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("SubscribedTo");
|
||||
|
||||
b.Navigation("Subscriber");
|
||||
|
||||
b.Navigation("SubscriptionTier");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserProfileModel", b =>
|
||||
{
|
||||
b.HasOne("OnlyPrompt.Backend.Database.Models.UserModel", "User")
|
||||
.WithOne("Profile")
|
||||
.HasForeignKey("OnlyPrompt.Backend.Database.Models.UserProfileModel", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.CategoryModel", b =>
|
||||
{
|
||||
b.Navigation("Prompts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.PromptModel", b =>
|
||||
{
|
||||
b.Navigation("Reviews");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.SubscriptionTierModel", b =>
|
||||
{
|
||||
b.Navigation("Prompts");
|
||||
|
||||
b.Navigation("Subscriptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OnlyPrompt.Backend.Database.Models.UserModel", b =>
|
||||
{
|
||||
b.Navigation("Profile")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Prompts");
|
||||
|
||||
b.Navigation("Subscribers");
|
||||
|
||||
b.Navigation("Subscriptions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
37
OnlyPrompt.Backend/OnlyPrompt.Backend.csproj
Normal file
@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>aspnet-OnlyPrompt.Backend-dc36f9f9-a53c-4ee3-a0b4-3396ee55b7c8</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" NoWarn="NU1605" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.5" NoWarn="NU1605" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.1" />
|
||||
<PackageReference Include="Microsoft.Identity.Web.DownstreamApi" Version="3.14.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.13.22" />
|
||||
<PackageReference Include="Scalar.AspNetCore.Microsoft" Version="2.13.22" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
OnlyPrompt.Backend/OnlyPrompt.Backend.http
Normal file
@ -0,0 +1,6 @@
|
||||
@OnlyPrompt.Backend_HostAddress = http://localhost:5093
|
||||
|
||||
GET {{OnlyPrompt.Backend_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
57
OnlyPrompt.Backend/Program.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Identity.Web;
|
||||
using OnlyPrompt.Backend.Database;
|
||||
using OnlyPrompt.Backend.Database.Models;
|
||||
using OnlyPrompt.Backend.Services.Jwt;
|
||||
using OnlyPrompt.Backend.Utils;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
|
||||
builder.Services.AddDbContext<OnlyPromptContext>(opts =>
|
||||
{
|
||||
opts.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"));
|
||||
opts.UseLazyLoadingProxies();
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IPasswordHasher<UserModel>, PasswordHasher<UserModel>>();
|
||||
builder.Services.AddSingleton<ITokenService, JwtTokenService>();
|
||||
builder.Services.AddAutoMapper(AutoMapperSetup.Setup);
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, opts => {
|
||||
opts.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
if (context.Request.Cookies.ContainsKey("jwt"))
|
||||
context.Token = context.Request.Cookies["jwt"];
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
23
OnlyPrompt.Backend/Properties/launchSettings.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7163;http://localhost:5093"
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker",
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
||||
"ASPNETCORE_HTTP_PORTS": "8080"
|
||||
},
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
},
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json"
|
||||
}
|
||||
15
OnlyPrompt.Backend/Services/Jwt/ITokenService.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using OnlyPrompt.Backend.Database.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OnlyPrompt.Backend.Services.Jwt
|
||||
{
|
||||
public interface ITokenService
|
||||
{
|
||||
string BuildToken(UserModel user, out DateTime validUntil);
|
||||
bool ValidateToken(string token, out ClaimsPrincipal claims);
|
||||
}
|
||||
}
|
||||
72
OnlyPrompt.Backend/Services/Jwt/JwtTokenService.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using OnlyPrompt.Backend.Database.Models;
|
||||
using OnlyPrompt.Backend.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OnlyPrompt.Backend.Services.Jwt
|
||||
{
|
||||
public class JwtTokenService : ITokenService
|
||||
{
|
||||
private string _key;
|
||||
private string _issuer;
|
||||
private string _audience;
|
||||
private TimeSpan _valid;
|
||||
|
||||
public JwtTokenService(IConfiguration config)
|
||||
{
|
||||
config = config.GetSection("Jwt");
|
||||
_key = config["Key"];
|
||||
_issuer = config["Issuer"];
|
||||
_audience = config["Audience"];
|
||||
_valid = config.GetValue<TimeSpan>("Valid");
|
||||
}
|
||||
|
||||
public string BuildToken(UserModel user, out DateTime validUntil)
|
||||
{
|
||||
var claims = user.GetClaims().ToList();
|
||||
validUntil = DateTime.UtcNow.Add(_valid);
|
||||
claims.Add(new Claim("exp", new DateTimeOffset(validUntil).ToUnixTimeSeconds().ToString()));
|
||||
claims.Add(new Claim("amr", "pwd"));
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_key));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
|
||||
var tokenDescriptor = new JwtSecurityToken(_issuer, _audience, claims, expires: validUntil, signingCredentials: credentials);
|
||||
return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
|
||||
}
|
||||
|
||||
public bool ValidateToken(string token, out ClaimsPrincipal claims)
|
||||
{
|
||||
var mySecret = Encoding.UTF8.GetBytes(_key);
|
||||
var mySecurityKey = new SymmetricSecurityKey(mySecret);
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
try
|
||||
{
|
||||
claims = tokenHandler.ValidateToken(token,
|
||||
new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidIssuer = _issuer,
|
||||
ValidAudience = _audience,
|
||||
IssuerSigningKey = mySecurityKey,
|
||||
}, out SecurityToken validatedToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
claims = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
OnlyPrompt.Backend/Utils/AutoMapperSetup.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using AutoMapper;
|
||||
using OnlyPrompt.Backend.ApiModels.Auth;
|
||||
using OnlyPrompt.Backend.ApiModels.UserProfile;
|
||||
using OnlyPrompt.Backend.Database.Models;
|
||||
|
||||
namespace OnlyPrompt.Backend.Utils
|
||||
{
|
||||
public static class AutoMapperSetup
|
||||
{
|
||||
public static void Setup(IMapperConfigurationExpression config)
|
||||
{
|
||||
config.CreateMap<UserModel, ApiUser>()
|
||||
.MapCtorParamFrom(x => x.Id, x => x.Id)
|
||||
.MapCtorParamFrom(x => x.UserName, x => x.UserName)
|
||||
.MapCtorParamFrom(x => x.Roles, x => x.Roles)
|
||||
.MapCtorParamFrom(x => x.Email, x => x.Email);
|
||||
|
||||
config.CreateMap<UserProfileModel, ApiUserProfile>()
|
||||
.MapCtorParamFrom(x => x.DisplayName, x => x.DisplayName)
|
||||
.MapCtorParamFrom(x => x.Slug, x => x.Slug)
|
||||
.MapCtorParamFrom(x => x.Bio, x => x.Bio)
|
||||
.MapCtorParamFrom(x => x.AvatarUrl, x => x.AvatarUrl)
|
||||
.MapCtorParamFrom(x => x.Specialities, x => x.Specialities)
|
||||
.MapCtorParamFrom(x => x.AverageRating, x => x.User.Prompts.Average(p => p.Reviews.Average(r => r.Rating)))
|
||||
.MapCtorParamFrom(x => x.Subscribers, x => x.User.Subscribers.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
46
OnlyPrompt.Backend/Utils/AutomapperExtensions.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using AutoMapper;
|
||||
using AutoMapper.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OnlyPrompt.Backend.Utils
|
||||
{
|
||||
public static class AutomapperExtensions
|
||||
{
|
||||
|
||||
public static IMappingExpression<TSource, TDestination> CreateUpdateMap<TSource, TDestination>(this IMapperConfigurationExpression cfg, MemberList memberList = MemberList.Source)
|
||||
{
|
||||
return cfg.CreateMap<TSource, TDestination>(memberList)
|
||||
.IgnoreNullMembers();
|
||||
}
|
||||
|
||||
public static IMappingExpression<TSource, TDestination> MapMemberFrom<TSource, TDestination, TMember, TSourceMember>(this IMappingExpression<TSource, TDestination> mapping, Expression<Func<TDestination, TMember>> destinationMember, Expression<Func<TSource, TSourceMember>> sourceMember)
|
||||
{
|
||||
mapping.ForMember(destinationMember, x => x.MapFrom(sourceMember));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
public static IMappingExpression<TSource, TDestination> IgnoreNullMembers<TSource, TDestination>(this IMappingExpression<TSource, TDestination> mapping)
|
||||
{
|
||||
mapping.ForAllMembers(opts => opts.Condition((src, dest, member) => src != null));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
public static IMappingExpression<TSource, TDestination> MapCtorParamFrom<TSource, TDestination, TMember, TSourceMember>(this IMappingExpression<TSource, TDestination> mapping, Expression<Func<TDestination, TMember>> destinationMember, Expression<Func<TSource, TSourceMember>> sourceMember)
|
||||
{
|
||||
mapping.ForCtorParam(destinationMember, x => x.MapFrom(sourceMember));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
public static IMappingExpression<TSource, TDestination> ForCtorParam<TSource, TDestination, DValue>(this IMappingExpression<TSource, TDestination> mapping, Expression<Func<TDestination, DValue>> paramSelector, Action<ICtorParamConfigurationExpression<TSource>> configure)
|
||||
{
|
||||
var ctorParamName = ((MemberExpression)paramSelector.Body).Member.Name;
|
||||
mapping.ForCtorParam(ctorParamName, configure);
|
||||
return mapping;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
OnlyPrompt.Backend/Utils/EntityExtensions.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OnlyPrompt.Backend.Database.Core;
|
||||
|
||||
namespace OnlyPrompt.Backend.Utils
|
||||
{
|
||||
public static class EntityExtensions
|
||||
{
|
||||
public static Task<T?> FindBySlugAsync<T>(this IQueryable<T> queryable, string slug) where T : class, IHasSlug
|
||||
{
|
||||
return queryable.FirstOrDefaultAsync(e => e.Slug == slug);
|
||||
}
|
||||
|
||||
public static Task<T?> FindByIdentifierAsync<T>(this IQueryable<T> queryable, Identifier identifier) where T : class, IHasSlug, IEntity
|
||||
{
|
||||
if (identifier.Id.HasValue)
|
||||
return queryable.FirstOrDefaultAsync(e => e.Id == identifier.Id.Value);
|
||||
|
||||
return queryable.FindBySlugAsync(identifier.Slug);
|
||||
}
|
||||
|
||||
public static IQueryable<T> OfIdentifer<T>(this IQueryable<T> queryable, Identifier identifier) where T : class, IHasSlug, IEntity
|
||||
{
|
||||
if (identifier.Id.HasValue)
|
||||
return queryable.Where(e => e.Id == identifier.Id.Value);
|
||||
return queryable.Where(e => e.Slug == identifier.Slug);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
OnlyPrompt.Backend/Utils/Extensions.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using OnlyPrompt.Backend.Database.Models;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace OnlyPrompt.Backend.Utils
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string? GetIdentifier(this ClaimsPrincipal principal) => principal.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
public static bool TryGetIdentifier(this ClaimsPrincipal principal, [NotNullWhen(true)]out string? identifier)
|
||||
{
|
||||
identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return identifier != null;
|
||||
}
|
||||
|
||||
public static Guid? GetUserId(this ClaimsPrincipal principal)
|
||||
{
|
||||
if (principal.TryGetIdentifier(out var identifier) && Guid.TryParse(identifier, out var userId))
|
||||
return userId;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IEnumerable<Claim> GetClaims(this UserModel user)
|
||||
{
|
||||
yield return new Claim(ClaimTypes.NameIdentifier, user.Id.ToString());
|
||||
yield return new Claim(ClaimTypes.Name, user.UserName);
|
||||
yield return new Claim(ClaimTypes.Email, user.Email);
|
||||
|
||||
foreach (var role in user.Roles)
|
||||
yield return new Claim(ClaimTypes.Role, role);
|
||||
}
|
||||
|
||||
public static CookieOptions Copy(this CookieOptions options, Action<CookieOptions>? modify = null)
|
||||
{
|
||||
var newOptions = new CookieOptions
|
||||
{
|
||||
Domain = options.Domain,
|
||||
Expires = options.Expires,
|
||||
HttpOnly = options.HttpOnly,
|
||||
IsEssential = options.IsEssential,
|
||||
MaxAge = options.MaxAge,
|
||||
Path = options.Path,
|
||||
SameSite = options.SameSite,
|
||||
Secure = options.Secure
|
||||
};
|
||||
|
||||
modify?.Invoke(newOptions);
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
public static string Limit(this string @string, int maxLength)
|
||||
{
|
||||
if (@string.Length <= maxLength)
|
||||
return @string;
|
||||
return @string.Substring(0, maxLength);
|
||||
}
|
||||
|
||||
public const string LowerAlphabet = "abcdefghijklmnopqrstuvwxyz";
|
||||
public static string GetString(this Random @random, int lenght, string alphabet = LowerAlphabet)
|
||||
{
|
||||
Span<char> chars = stackalloc char[lenght];
|
||||
for (int i = 0; i < lenght; i++)
|
||||
chars[i] = alphabet[@random.Next(alphabet.Length)];
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
OnlyPrompt.Backend/Utils/Identifier.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Scalar.AspNetCore;
|
||||
|
||||
namespace OnlyPrompt.Backend.Utils
|
||||
{
|
||||
public struct Identifier
|
||||
{
|
||||
public string Slug { get; init; }
|
||||
public Guid? Id { get; init; }
|
||||
|
||||
public Identifier(string slug)
|
||||
{
|
||||
this.Slug = slug;
|
||||
this.Id = null;
|
||||
}
|
||||
|
||||
public Identifier(Guid guid)
|
||||
{
|
||||
this.Id = guid;
|
||||
this.Slug = string.Empty;
|
||||
}
|
||||
|
||||
public static implicit operator Identifier(string slug) => new Identifier(slug);
|
||||
public static implicit operator Identifier(Guid guid) => new Identifier(guid);
|
||||
|
||||
public static bool TryParse(string input, out Identifier identifier)
|
||||
{
|
||||
identifier= new Identifier(input);
|
||||
if (Guid.TryParse(input, out var guid))
|
||||
identifier = new Identifier(guid);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
OnlyPrompt.Backend/Utils/SlugHelper.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace OnlyPrompt.Backend.Utils
|
||||
{
|
||||
public static class SlugHelper
|
||||
{
|
||||
private static readonly Regex InvalidCharacters = new(@"[^a-z0-9\-]", RegexOptions.Compiled);
|
||||
private static readonly Regex MultipleDashes = new(@"-+", RegexOptions.Compiled);
|
||||
|
||||
public static string GenerateSlug(string input, int? maxLength = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return string.Empty;
|
||||
|
||||
var slug = input.ToLowerInvariant().Replace(" ", "-").Replace("_", "-");
|
||||
slug = InvalidCharacters.Replace(slug, string.Empty);
|
||||
slug = MultipleDashes.Replace(slug, "-");
|
||||
slug = slug.Trim('-');
|
||||
return slug;
|
||||
}
|
||||
|
||||
private const string SuffixChars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
public static async Task<string> GenerateUniqueSlug(string input, Func<string, Task<bool>> existsFunc, int? maxLenght)
|
||||
{
|
||||
var baseSlug = GenerateSlug(input, maxLenght - 9);
|
||||
var slug = baseSlug;
|
||||
var suffix = Random.Shared.GetString(8, SuffixChars);
|
||||
return $"{slug}-{suffix}";
|
||||
}
|
||||
}
|
||||
}
|
||||
8
OnlyPrompt.Backend/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
OnlyPrompt.Backend/appsettings.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Include Error Detail=true;User ID=onlyprompt;Password=onlyprompt;Host=postgres;Port=1803;Database=onlyprompt;Pooling=true;MinPoolSize=0;MaxPoolSize=100;Connection Lifetime=0;"
|
||||
},
|
||||
"Jwt": {
|
||||
"Issuer": "https://onlyprompts.com",
|
||||
"Audience": "https://onlyprompts.com",
|
||||
"Key": "TfZi@!CC!b5UoD81gs&%tvY4J0M$p3cI",
|
||||
"Valid": "3.0:0:0"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 579 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 660 KiB After Width: | Height: | Size: 660 KiB |
|
Before Width: | Height: | Size: 4.1 MiB After Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 659 KiB |
|
Before Width: | Height: | Size: 334 KiB After Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 868 KiB After Width: | Height: | Size: 868 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
6
OnlyPrompt.slnx
Normal file
@ -0,0 +1,6 @@
|
||||
<Solution>
|
||||
<Project Path="docker-compose.dcproj" Id="ee26a7fb-2dee-47a7-964c-12ef03b1114a">
|
||||
<Build />
|
||||
</Project>
|
||||
<Project Path="OnlyPrompt.Backend/OnlyPrompt.Backend.csproj" />
|
||||
</Solution>
|
||||
17
docker-compose.dcproj
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectVersion>2.1</ProjectVersion>
|
||||
<DockerTargetOS>Linux</DockerTargetOS>
|
||||
<ProjectGuid>ee26a7fb-2dee-47a7-964c-12ef03b1114a</ProjectGuid>
|
||||
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
|
||||
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}/scalar</DockerServiceUrl>
|
||||
<DockerServiceName>documentapi</DockerServiceName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include=".env" />
|
||||
<None Include="docker-compose.debug.yml" />
|
||||
<None Include="docker-compose.yml" />
|
||||
<None Include=".dockerignore" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
15
docker-compose.debug.yml
Normal file
@ -0,0 +1,15 @@
|
||||
services:
|
||||
database:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
volumes:
|
||||
- 'database_data:/var/lib/postgresql'
|
||||
ports:
|
||||
- "${PORT_PREFIX}3:5432"
|
||||
|
||||
volumes:
|
||||
database_data:
|
||||
27
docker-compose.yml
Normal file
@ -0,0 +1,27 @@
|
||||
services:
|
||||
onlyprompt.backend:
|
||||
image: ${DOCKER_REGISTRY-}onlyprompt
|
||||
build:
|
||||
context: .
|
||||
dockerfile: OnlyPrompt.Backend/Dockerfile
|
||||
ports:
|
||||
- "${PORT_PREFIX}1:8080"
|
||||
- "${PORT_PREFIX}2:8081"
|
||||
environment:
|
||||
JWT__ISSUER: "https://onlyprompt.com"
|
||||
CONNECTIONSTRINGS__DEFAULT: "Include Error Detail=true;User ID=${DB_USER};Password=${DB_PASSWORD};Host=postgres;Port=5432;Database=${DB_NAME};Pooling=true;MinPoolSize=0;MaxPoolSize=100;Connection Lifetime=0;"
|
||||
ASPNETCORE_URLS: "http://*:8080"
|
||||
ASPNETCORE_ENVIRONMENT: "Development"
|
||||
|
||||
database:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
volumes:
|
||||
- 'database_data:/var/lib/postgresql'
|
||||
|
||||
volumes:
|
||||
database_data:
|
||||