Project Use-Case

This project was started to help me regain my technical edge when applying for engineering roles after spending close to five years in non-engineering positions.

One of the key requirements was to include as many aspects of engineering as possible. I achieved this by building a complete full stack hosted on a VPS, forcing me to learn modern hosting practices, manage my own data layer, and ensure secure communication between the backend and frontend.

Project Challenges

The main challenges were managing multiple authentication systems, getting up to speed with active threats, and preventing frequent spam posts from bots.

These were solved through a fair amount of trial and error (especially around authentication). It’s not 100% perfect, but it now fails gracefully rather than exposing vulnerabilities. Spam posting is mitigated by requiring accounts for commenting, and accounts must be activated before users can interact with the site.

Bots hammering known vulnerabilities (e.g. wp-admin, generic PHP requests, etc.) are handled at the Nginx level by dropping those requests before they reach the application.

Another major challenge was search optimisation and link sharing. Platforms like LinkedIn need to discover images, titles, and metadata, which is difficult when the frontend is 100% JavaScript.

This was solved by introducing an og/ endpoint that returns the required metadata for crawlers, while human users are redirected to the actual content.

Project Technical Stack

Frontend — carlsson-tech-ui

Component Technology
Framework Angular 21 (standalone + module hybrid)
Language TypeScript 5.9
UI Libraries Bootstrap 5, NgBootstrap 20, UIKit 3
Authentication MSAL (Microsoft), Auth0 JWT, angular-oauth2-oidc
Markdown Rendering ngx-markdown, Prism.js
HTTP Angular HttpClient with JWT interceptor
State Management RxJS 7

Backend — carlsson-tech-api

Component Technology
Framework ASP.NET Core 9
Language C# (.NET 9)
ORM Entity Framework Core 9
Database Microsoft SQL Server
Authentication JWT Bearer, hand-rolled OAuth (GitHub, Microsoft, LinkedIn)
Image Processing SixLabors.ImageSharp 3
Email Resend API
Captcha hCaptcha
API Documentation Swagger / OpenAPI (Swashbuckle)
Containerization Docker (multi-stage build)

Architecture

The platform follows a client–server architecture with a clear separation between a stateless single-page frontend and a stateful RESTful backend API.

Frontend (carlsson-tech-ui)

The Angular SPA communicates exclusively with the backend via versioned REST endpoints (/v1/) over HTTPS. A JWT interceptor automatically attaches Bearer tokens to outbound requests, while a dedicated error interceptor handles 401/403 responses.

The application is organised into domain-scoped feature modules covering authentication, blog authoring, comments, user profiles, admin tooling, and media management. The build outputs a static asset bundle served independently (e.g. via Nginx or a CDN).

Backend (carlsson-tech-api)

The API follows a classic layered pattern: controllers delegate to business logic, which interacts with the database through an EF Core ApplicationDbContext.

Key design decisions include:

  • Authentication: JWT access tokens (1-hour expiry) paired with database-tracked refresh tokens for rotation. OAuth flows for GitHub, Microsoft, and LinkedIn are implemented natively without a third-party identity framework.
  • Authorisation: A role-based access control model using a GroupPermission bitmask enum. Permissions are enforced via a custom [RequirePermission] attribute on controller actions. Seven permission tiers exist, ranging from Guest to Admin.
  • User model: Table-per-Type (TPT) inheritance distinguishes LocalUser (email/password) from OAuthUser (provider + subject ID) within the same user hierarchy.
  • Media: Blog images and user avatars are processed server-side using ImageSharp (crop, resize, format conversion) and stored on the file system under wwwroot.
  • Content moderation: A reporting system allows posts and comments to be flagged, with a dedicated review workflow for moderators.
  • CORS: Restricted to *.carlsson.tech subdomains and localhost.

Data Layer

A single SQL Server Express instance hosts the application database, covering users, posts, comments, tags, reports, projects, and questionnaires. EF Core migrations manage schema evolution.

Server Stack

Nginx acts as the sole public-facing entry point, handling TLS termination (Let's Encrypt certificates with Diffie-Hellman key exchange), HTTP-to-HTTPS redirects, and www-to-non-www canonicalisation.

Beyond routing, Nginx also provides an active layer of threat mitigation:

  • Vulnerability probe blocking: Requests targeting common attack surfaces — WordPress paths (/wp-admin, xmlrpc.php), PHP files, phpMyAdmin panels, backup file extensions (.bak, .sql, .dump), and exposed dotfiles/directories (.git, .ssh, .aws, .htaccess) — are terminated with a 404 before reaching the application.
  • Sensitive file protection: Requests for .json, .config, .env, .log, and .ini files are denied outright.
  • IP blocklist: A dynamically generated blockips.conf is included at runtime, maintained by the IP management layer and reloaded via systemctl reload nginx without service interruption.
  • Static asset caching: Frontend assets (JS, CSS, fonts, images) are served with a one-year Cache-Control header, reducing load on the application.
  • Sitemap proxying with cache: /sitemap.xml is generated dynamically by the backend and proxied through Nginx, which caches the response for 24 hours. To the client, it behaves like a static file.
  • OG preview redirect: Requests to /read/:slug from known social media crawlers (LinkedIn, Twitter, Facebook, Discord, Slack, Google) are redirected to the API’s /v1/blog/og/:slug endpoint, which returns pre-rendered Open Graph metadata. Human visitors are served the Angular SPA as normal.
  • SPA fallback: All unmatched routes fall through to index.html, enabling Angular’s client-side routing.

Deployment

The API is deployed as a Docker container (ASP.NET Core runtime, port 5000). The frontend is deployed as a static SPA build. Configuration is environment-driven via layered appsettings.json files and environment variables, with secrets injected at runtime in production.

Deployments are performed via privileged Bash scripts (deploy-backend.sh, deploy-frontend.sh) run manually over SSH.

Backend deployment

  1. The API systemd service is stopped cleanly before any files are touched.
  2. The wwwroot directory (user-uploaded images and assets) is backed up to a timestamped archive to prevent data loss.
  3. The target directory is cleared and replaced with the new publish output.
  4. wwwroot is restored from the backup.
  5. File ownership is set to www-data before restarting the service, preventing permission issues.
  6. The service is started and its status is printed immediately.

Frontend deployment

  1. robots.txt and sitemap.xml are preserved to /tmp before the web root is cleared, as they are managed separately from the Angular build.
  2. The new build is copied into place and ownership is set to www-data.
  3. Preserved files are restored.

The scripts are not exposed to the web. They are executed manually by an authenticated user with sudo access. No deployment tooling, webhooks, or remote execution endpoints are exposed, eliminating that attack surface entirely.

Project Info

Author:

Published:

Views:
48

Tags:
Share on:

Ads by Google AdSense