Skip to content

Commit

Permalink
Merge pull request #2 from DominicMaas/dynamic
Browse files Browse the repository at this point in the history
Dynamic Content
  • Loading branch information
DominicMaas authored Mar 29, 2024
2 parents 4a4293b + 7815137 commit 633a110
Show file tree
Hide file tree
Showing 36 changed files with 1,391 additions and 55 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,15 @@ My personal website

## Technologies
- ASP.NET Core
- Docker
- Docker
- Azure Key Vault
- CloudFlare R2 (via AWS SDK)

## Neso

Neso is the VPS that this site runs off of. It runs this website, a docker registry, watchtower a SQLite browser all within docker.

## Database Platform

Dynamic data is stored within a SQLite database stored on Neso. [Lightstream](https://litestream.io/) is used to replicate the database.

6 changes: 0 additions & 6 deletions Website.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Website.Core", "src\Website.Core\Website.Core.csproj", "{1B67DC50-C76D-4853-8211-3BE2BA353DFB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -22,10 +20,6 @@ Global
{92587D00-4E5F-4682-8A28-130AC04430DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92587D00-4E5F-4682-8A28-130AC04430DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92587D00-4E5F-4682-8A28-130AC04430DF}.Release|Any CPU.Build.0 = Release|Any CPU
{1B67DC50-C76D-4853-8211-3BE2BA353DFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B67DC50-C76D-4853-8211-3BE2BA353DFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B67DC50-C76D-4853-8211-3BE2BA353DFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B67DC50-C76D-4853-8211-3BE2BA353DFB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
9 changes: 0 additions & 9 deletions src/Website.Core/Website.Core.csproj

This file was deleted.

6 changes: 6 additions & 0 deletions src/Website/Common/DatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ public class DatabaseContext : DbContext
{
public DbSet<ShortLink> ShortLinks { get; set; }

public DbSet<ShortLinkHit> ShortLinkHits { get; set; }

public DbSet<StreamPost> Streams { get; set; }

public DbSet<Image> Images { get; set; }

public DatabaseContext()
{ }

Expand Down
22 changes: 22 additions & 0 deletions src/Website/Controllers/AuthenticationController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;

namespace Website.Controllers;

public class AuthenticationController : Controller
{
[HttpGet("~/signin")]
public IActionResult SignIn()
{
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, MicrosoftAccountDefaults.AuthenticationScheme);
}

[HttpGet("~/signout")]
[HttpPost("~/signout")]
public IActionResult SignOutCurrentUser()
{
return SignOut(new AuthenticationProperties { RedirectUri = "/" }, CookieAuthenticationDefaults.AuthenticationScheme);
}
}
217 changes: 217 additions & 0 deletions src/Website/Controllers/ImagesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Website.Common;
using Website.Services;
using Htmx;
using Image = Website.Models.Database.Image;

namespace Website.Controllers;

[Authorize]
[Route("admin/images")]
public class ImagesController : Controller
{
private readonly DatabaseContext _context;
private readonly R2 _r2;
private readonly ILogger<ImagesController> _logger;

public ImagesController(DatabaseContext context, R2 r2, ILogger<ImagesController> logger)
{
_context = context;
_r2 = r2;
_logger = logger;
}

[HttpGet]
public async Task<IActionResult> Index()
{
return View(await _context.Images.ToListAsync());
}

[HttpGet("upload")]
public IActionResult Upload()
{
return View();
}

[HttpPost("upload")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Upload([Bind("DateTaken,Description,ImageFile")] ImageUpload imageUpload, CancellationToken cancellationToken)
{
// Run basic validation
if (!ModelState.IsValid) return View(imageUpload);

// Ensure we have an image file
if (imageUpload.ImageFile == null)
{
ModelState.AddModelError(nameof(imageUpload.ImageFile), "An image file is required");
return View(imageUpload);
}

// TODO: Ensure image was actually uploaded

using var imageStream = new MemoryStream();

try
{
// Start processing the uploaded image. We want to strip meta data, and convert to a jpeg. we also
// want to create a thumbnail as well.
using var image = SixLabors.ImageSharp.Image.Load(imageUpload.ImageFile.OpenReadStream());

// Resize to an appropriate max size of 1000px
image.Mutate(x => x.Resize(new ResizeOptions { Mode = ResizeMode.Max, Size = new Size(1000) }));

image.Metadata.ExifProfile = null;
image.Metadata.XmpProfile = null;

// Save the image to a memory stream
await image.SaveAsJpegAsync(imageStream, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder { Quality = 80 }, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while processing an image");

ModelState.AddModelError(nameof(imageUpload.ImageFile), "An error occurred while processing this image. Please try again later.");
return View(imageUpload);
}

// Handle cancel during processing
if (cancellationToken.IsCancellationRequested)
{
ModelState.AddModelError(string.Empty, "The upload was canceled!");
return View(imageUpload);
}

imageUpload.Id = Guid.NewGuid();
imageUpload.DateUploaded = DateTime.UtcNow;

try
{
// Attempt to upload the image
await _r2.UploadImageAsync(imageStream, $"i/{imageUpload.Id}.jpg", "image/jpeg", cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while uploading an image to R2");

ModelState.AddModelError(nameof(imageUpload.ImageFile), "An error occurred while uploading this image to R2. Please try again later.");
return View(imageUpload);
}

// Handle cancel during R2 upload
if (cancellationToken.IsCancellationRequested)
{
ModelState.AddModelError(string.Empty, "The upload was canceled!");
return View(imageUpload);
}

_context.Add(imageUpload);
await _context.SaveChangesAsync(cancellationToken);
return RedirectToAction(nameof(Index));
}

[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(Guid? id)
{
if (id == null)
{
return NotFound();
}

var image = await _context.Images.FindAsync(id);
if (image == null)
{
return NotFound();
}
return View(image);
}

[HttpPost("edit/{id}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, [Bind("DateTaken,Description")] Image image)
{
if (id != image.Id)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(image);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ImageExists(image.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(image);
}

[HttpPost("delete/{id}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Guid? id, CancellationToken cancellationToken)
{
if (id == null)
{
return NotFound();
}

var image = await _context.Images.FirstOrDefaultAsync(m => m.Id == id, cancellationToken);
if (image == null)
{
return NotFound();
}

// TODO: Check if used in streams or gallery

try
{
await _r2.DeleteImageAsync($"i/{image.Id}.jpg", cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while deleting an image from R2");
return NotFound();
}

_context.Images.Remove(image);
await _context.SaveChangesAsync(cancellationToken);

Response.Htmx(headers =>
{
headers.Refresh();
});

return Ok();
}

private bool ImageExists(Guid id)
{
return _context.Images.Any(e => e.Id == id);
}

public class ImageUpload : Image
{
[NotMapped]
[DisplayName("Image")]
[Required(ErrorMessage = "An image file is required")]
public IFormFile? ImageFile { get; set; }
}
}
82 changes: 82 additions & 0 deletions src/Website/Controllers/RSSController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ServiceModel.Syndication;
using System.Text;
using System.Xml;
using Website.Common;

namespace Website.Controllers;

public class RSSController : Controller
{
private readonly DatabaseContext _context;

public RSSController(DatabaseContext context)
{
_context = context;
}

[ResponseCache(Duration = 1200)]
[HttpGet("/feed/stream.xml")]
public async Task<IActionResult> StreamRSSAsync()
{
var latestStream = await _context.Streams.OrderByDescending(x => x.Posted).FirstOrDefaultAsync();

var feed = BuildBasicFeed("Stream", "Quick thoughts and ideas", new("https://dominicmaas.co.nz/feed/stream.xml"), latestStream?.Posted ?? default);
var items = new List<SyndicationItem>();


var streams = await _context.Streams.OrderByDescending(x => x.Posted).Take(20).ToListAsync();
foreach (var stream in streams)
{
items.Add(new SyndicationItem(stream.Title, stream.Content, new Uri($"https://dominicmaas.co.nz/stream/{stream.Id}"), stream.Id.ToString(), stream.Posted));
}

feed.Items = items;

return BuildSyndicationFeed(feed);
}

//[ResponseCache(Duration = 1200)]
//[HttpGet("/feed/blog.xml")]
//public IActionResult BlogRSS()
//{
// var feed = BuildBasicFeed("Blog", "Long form posts or structured content", new("https://dominicmaas.co.nz/feed/blog.xml"));

// var testStream = new SyndicationItem("This is a test blog", "This is the content", new Uri("https://dominicmaas.co.nz/blog/123"), "123", DateTimeOffset.Now);
// feed.Items = new[] { testStream };

// return BuildSyndicationFeed(feed);
//}

private static SyndicationFeed BuildBasicFeed(string name, string description, Uri url, DateTime lastUpdated)
{
var feed = new SyndicationFeed($"Dominic Maas - {name}", description, url);
feed.Authors.Add(new SyndicationPerson("[email protected]", "Dominic Maas", "https://dominicmaas.co.nz"));
feed.Copyright = new TextSyndicationContent($"{DateTime.Now.Year} Dominic Maas");
feed.ImageUrl = new Uri("https://dominicmaas.co.nz/favicon.ico");
feed.LastUpdatedTime = lastUpdated;

return feed;
}

private FileContentResult BuildSyndicationFeed(SyndicationFeed feed)
{
var settings = new XmlWriterSettings
{
Encoding = Encoding.UTF8,
NewLineHandling = NewLineHandling.Entitize,
NewLineOnAttributes = true,
Indent = true
};

using var stream = new MemoryStream();
using var xmlWriter = XmlWriter.Create(stream, settings);

var rssFormatter = new Rss20FeedFormatter(feed, false);
rssFormatter.WriteTo(xmlWriter);
xmlWriter.Flush();

return File(stream.ToArray(), "application/rss+xml; charset=utf-8");
}
}
Loading

0 comments on commit 633a110

Please sign in to comment.