420 lines
13 KiB
C#

using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OnlyPrompt.Backend.ApiModels.Prompt;
using OnlyPrompt.Backend.ApiModels.UserProfile;
using OnlyPrompt.Backend.Database;
using OnlyPrompt.Backend.Database.Models;
using OnlyPrompt.Backend.Utils;
using System.ComponentModel.DataAnnotations;
namespace OnlyPrompt.Backend.Controllers
{
[ApiController]
[Route("api/v1/prompts")]
[Authorize(Roles = ModelConstants.UserRole)]
public class PromptController : BaseController
{
public PromptController(OnlyPromptContext db, IMapper mapper) : base(db, mapper)
{
}
private IQueryable<PromptModel> GetAccessiblePrompts(Guid userId)
{
return _db.Prompts.Where(
p => p.SubscriptionTier == null
|| p.Creator.Subscribers.Any(
sub => sub.SubscriberId == userId
&& p.SubscriptionTier!.Level <= sub.SubscriptionTier!.Level
)
);
}
[HttpGet]
public async Task<ApiMinimalPrompt[]> GetPromptsAsync(
[Range(0, int.MaxValue)][FromQuery] int offset = 0,
[Range(1, 100)][FromQuery] int limit = 20,
[FromQuery] FeedSortType sortBy = FeedSortType.Date,
[FromQuery] bool ascending = false,
[FromQuery] Identifier? category = null,
[FromQuery] string? search = null
)
{
var userId = User.GetUserId();
var query = _db.Prompts
.Where(x => x.CreatorId != userId);
if (category.HasValue)
query = query.Where(x => category.Value.Id.HasValue ? x.CategoryId == category.Value.Id.Value : x.Category.Slug == category.Value.Slug);
if (!string.IsNullOrWhiteSpace(search))
query = query.Where(x => x.Title.Contains(search) || x.Description.Contains(search));
query = sortBy switch
{
FeedSortType.Date => query.OrderBy(x => x.UpdatedAt, ascending),
FeedSortType.Rating => ascending
? query.OrderBy(x => x.Reviews.Average(r => (double?)r.Rating) ?? 0).ThenBy(x => x.Reviews.Count)
: query.OrderByDescending(x => x.Reviews.Average(r => (double?)r.Rating) ?? 0).ThenByDescending(x => x.Reviews.Count),
_ => query
};
var prompts = await query
.Skip(offset)
.Take(limit)
.Select(x => new ApiMinimalPrompt(
x.Id,
x.Title,
x.Description,
x.UpdatedAt,
x.CreatorId,
x.Creator.Profile.DisplayName,
x.Creator.Profile.AvatarUrl,
x.ExampleImageUrl,
x.Price,
x.Likes.Count,
x.Likes.Any(l => l.UserId == userId),
x.Saves.Count,
x.Saves.Any(s => s.UserId == userId),
x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level,
x.SubscriptionTier == null ? null : x.SubscriptionTier.Name,
x.SubscriptionTier == null ? (decimal?)null : x.SubscriptionTier.MonthlyPrice,
x.Reviews.Average(r => (double?)r.Rating),
x.Reviews.Count,
x.CreatorId == userId || x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level <= s.SubscriptionTier.Level)
)).ToArrayAsync();
return prompts;
}
[HttpGet("mine")]
public async Task<ApiMinimalPrompt[]> GetOwnPromptsAsync()
{
var userId = User.GetUserId();
var prompts = await _db.Prompts
.Where(x => x.CreatorId == userId)
.OrderByDescending(x => x.UpdatedAt)
.Select(x => new ApiMinimalPrompt(
x.Id,
x.Title,
x.Description,
x.UpdatedAt,
x.CreatorId,
x.Creator.Profile.DisplayName,
x.Creator.Profile.AvatarUrl,
x.ExampleImageUrl,
x.Price,
x.Likes.Count,
x.Likes.Any(l => l.UserId == userId),
x.Saves.Count,
x.Saves.Any(s => s.UserId == userId),
x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level,
x.SubscriptionTier == null ? null : x.SubscriptionTier.Name,
x.SubscriptionTier == null ? (decimal?)null : x.SubscriptionTier.MonthlyPrice,
x.Reviews.Average(r => (double?)r.Rating),
x.Reviews.Count,
true
)).ToArrayAsync();
return prompts;
}
[HttpGet("{id}")]
public async Task<Results<Ok<ApiPrompt>, NotFound<string>>> GetPromptAsync(Identifier id)
{
var userId = User.GetUserId();
var prompt = await _db.Prompts
.OfIdentifer(id)
.FirstOrDefaultAsync();
if (prompt is null)
return TypedResults.NotFound("Prompt not found");
var canAccess = await GetAccessiblePrompts(userId.Value).AnyAsync(p => p.Id == prompt.Id);
var apiPrompt = _mapper.Map<ApiPrompt>(prompt) with
{
Content = canAccess ? prompt.Prompt : null,
CanAccess = canAccess,
IsLiked = prompt.Likes.Any(l => l.UserId == userId),
LikeCount = prompt.Likes.Count,
IsSaved = prompt.Saves.Any(s => s.UserId == userId),
SaveCount = prompt.Saves.Count
};
return TypedResults.Ok(apiPrompt);
}
[HttpPut("{id}")]
public async Task<Results<Ok<ApiPrompt>, NotFound<string>>> UpdatePromptAsync(Identifier id, [FromBody] ApiUpdatePromptRequest request)
{
var userId = User.GetUserId();
var prompt = await _db.Prompts
.OfIdentifer(id)
.FirstOrDefaultAsync(p => p.CreatorId == userId);
if (prompt is null)
return TypedResults.NotFound("Prompt not found or no permission");
var category = await _db.Categories.FindByIdentifierAsync(new Identifier(request.Category));
if (category is null)
return TypedResults.NotFound("Category not found");
prompt.Title = request.Title;
prompt.Description = request.Description;
prompt.Prompt = request.Content;
prompt.Category = category;
prompt.ExampleOutput = request.ExampleOutput;
prompt.ExampleImageUrl = request.ExampleImageUrl;
prompt.Price = null;
prompt.SubscriptionTier = null;
if (request.SubscriptionTier.HasValue)
{
var subscriptionTier = await _db.SubscriptionTiers.FirstOrDefaultAsync(
t => t.Level == request.SubscriptionTier.Value
&& t.UserId == userId
);
if (subscriptionTier is null)
return TypedResults.NotFound("Subscription tier not found");
prompt.SubscriptionTier = subscriptionTier;
}
await _db.SaveChangesAsync();
var apiPrompt = _mapper.Map<ApiPrompt>(prompt) with { Content = prompt.Prompt, CanAccess = true };
return TypedResults.Ok(apiPrompt);
}
[HttpPut("{id}/likes")]
public async Task<Results<Ok<ApiLikeState>, NotFound<string>>> LikePromptAsync(Identifier id)
{
var userId = User.GetUserId();
var prompt = await _db.Prompts
.OfIdentifer(id)
.FirstOrDefaultAsync();
if (prompt is null)
return TypedResults.NotFound("Prompt not found");
var exists = await _db.PromptLikes.AnyAsync(l => l.PromptId == prompt.Id && l.UserId == userId);
if (exists == false)
{
_db.PromptLikes.Add(new PromptLikeModel
{
PromptId = prompt.Id,
UserId = userId!.Value
});
await _db.SaveChangesAsync();
}
var count = await _db.PromptLikes.CountAsync(l => l.PromptId == prompt.Id);
return TypedResults.Ok(new ApiLikeState(count, true));
}
[HttpDelete("{id}/likes")]
public async Task<Results<Ok<ApiLikeState>, NotFound<string>>> UnlikePromptAsync(Identifier id)
{
var userId = User.GetUserId();
var prompt = await _db.Prompts
.OfIdentifer(id)
.FirstOrDefaultAsync();
if (prompt is null)
return TypedResults.NotFound("Prompt not found");
await _db.PromptLikes
.Where(l => l.PromptId == prompt.Id && l.UserId == userId)
.ExecuteDeleteAsync();
var count = await _db.PromptLikes.CountAsync(l => l.PromptId == prompt.Id);
return TypedResults.Ok(new ApiLikeState(count, false));
}
[HttpPut("{id}/saves")]
public async Task<Results<Ok<ApiSaveState>, NotFound<string>>> SavePromptAsync(Identifier id)
{
var userId = User.GetUserId();
var prompt = await _db.Prompts
.OfIdentifer(id)
.FirstOrDefaultAsync();
if (prompt is null)
return TypedResults.NotFound("Prompt not found");
var exists = await _db.PromptSaves.AnyAsync(s => s.PromptId == prompt.Id && s.UserId == userId);
if (exists == false)
{
_db.PromptSaves.Add(new PromptSaveModel
{
PromptId = prompt.Id,
UserId = userId!.Value
});
await _db.SaveChangesAsync();
}
var count = await _db.PromptSaves.CountAsync(s => s.PromptId == prompt.Id);
return TypedResults.Ok(new ApiSaveState(count, true));
}
[HttpDelete("{id}/saves")]
public async Task<Results<Ok<ApiSaveState>, NotFound<string>>> UnsavePromptAsync(Identifier id)
{
var userId = User.GetUserId();
var prompt = await _db.Prompts
.OfIdentifer(id)
.FirstOrDefaultAsync();
if (prompt is null)
return TypedResults.NotFound("Prompt not found");
await _db.PromptSaves
.Where(s => s.PromptId == prompt.Id && s.UserId == userId)
.ExecuteDeleteAsync();
var count = await _db.PromptSaves.CountAsync(s => s.PromptId == prompt.Id);
return TypedResults.Ok(new ApiSaveState(count, false));
}
[HttpDelete("{id}")]
public async Task<Results<NoContent, NotFound<string>>> DeletePromptAsync(Identifier id)
{
var userId = User.GetUserId();
var isAdmin = User.IsInRole(ModelConstants.AdminRole);
var count = await _db.Prompts
.OfIdentifer(id)
.Where(p => p.CreatorId == userId || isAdmin)
.ExecuteDeleteAsync();
if (count == 0)
return TypedResults.NotFound("Prompt not found or no permission");
return TypedResults.NoContent();
}
[HttpPost]
public async Task<Results<Ok<ApiPrompt>, NotFound<string>>> CreatePromptAsync([FromBody] ApiCreatePromptRequest request)
{
var userId = User.GetUserId();
var category = await _db.Categories.FindByIdentifierAsync(new Identifier(request.Category));
if (category is null)
return TypedResults.NotFound("Category not found");
SubscriptionTierModel? subscriptionTier = null;
if (request.SubscriptionTier.HasValue)
{
subscriptionTier = await _db.SubscriptionTiers.FirstOrDefaultAsync(
t => t.Level == request.SubscriptionTier.Value
&& t.UserId == userId
);
if (subscriptionTier is null)
return TypedResults.NotFound("Subscription tier not found");
}
var slug = request.Slug;
if (string.IsNullOrEmpty(slug))
slug = await SlugHelper.GenerateUniqueSlugAsync(request.Title, slug => _db.Prompts.AnyAsync(p => p.Slug == slug), ModelConstants.MaxSlugLength);
var prompt = new PromptModel
{
Id = Guid.NewGuid(),
Title = request.Title,
Description = request.Description,
Prompt = request.Content,
ExampleOutput = request.ExampleOutput,
ExampleImageUrl = request.ExampleImageUrl,
Price = null,
CreatorId = userId.Value,
SubscriptionTier = subscriptionTier,
Category = category,
Slug = slug
};
_db.Prompts.Add(prompt);
await _db.SaveChangesAsync();
var apiPrompt = _mapper.Map<ApiPrompt>(prompt);
return TypedResults.Ok(apiPrompt);
}
[HttpGet("{id}/reviews")]
public async Task<Ok<ApiReview[]>> GetReviewsAsync(Identifier id, [FromQuery] int offset = 0, [Range(1, 200)][FromQuery] int limit = 20)
{
var userId = User.GetUserId();
var accessiblePrompts = GetAccessiblePrompts(userId!.Value);
var reviews = await accessiblePrompts
.OfIdentifer(id)
.SelectMany(x => x.Reviews)
.OrderByDescending(x => x.UpdatedAt)
.Skip(offset)
.Take(limit)
.ProjectTo<ApiReview>(_mapper.ConfigurationProvider)
.ToArrayAsync();
return TypedResults.Ok(reviews);
}
[HttpPut("{id}/reviews")]
public async Task<Results<Ok<ApiReview>, BadRequest<string>, NotFound<string>>> AddReviewAsync(Identifier id, [FromBody] ApiCreateReviewRequest request)
{
var userId = User.GetUserId();
var prompt = await GetAccessiblePrompts(userId!.Value)
.OfIdentifer(id)
.FirstOrDefaultAsync();
if (prompt is null)
return TypedResults.NotFound("Prompt not found or no permission");
if(prompt.CreatorId == userId)
return TypedResults.BadRequest("Cannot review your own prompt");
var review = await _db.Reviews.FirstOrDefaultAsync(
r => r.PromptId == prompt.Id
&& r.ReviewerId == userId
);
if (review is null)
{
review = new ReviewModel
{
PromptId = prompt.Id,
ReviewerId = userId.Value,
Comment = request.Comment,
Rating = request.Rating
};
_db.Reviews.Add(review);
}
else
{
review.Comment = request.Comment;
review.Rating = request.Rating;
}
await _db.SaveChangesAsync();
var apiReview = _mapper.Map<ApiReview>(review);
return TypedResults.Ok(apiReview);
}
[HttpDelete("{promptId}/reviews/{reviewerId}")]
public async Task<Results<NoContent, NotFound<string>>> DeleteReviewAsync(Identifier promptId, Guid reviewerId)
{
var userId = User.GetUserId();
var isAdmin = User.IsInRole(ModelConstants.AdminRole);
var count = await _db.Reviews
.Where(
r => (promptId.Id.HasValue ? r.PromptId == promptId.Id : r.Prompt.Slug == promptId.Slug)
&& (r.ReviewerId == reviewerId || isAdmin)
)
.ExecuteDeleteAsync();
if (count == 0)
return TypedResults.NotFound("Review not found or no permission");
return TypedResults.NoContent();
}
}
}