Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
node_modules
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*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
11 changes: 0 additions & 11 deletions .editorconfig

This file was deleted.

46 changes: 30 additions & 16 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
name: Docker
name: Build & Publish Docker Image

on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
branches: [main]
tags: ["v*"]

env:
REGISTRY: ghcr.io
IMAGE_NAME: backend-net

jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
push:
build-and-push:
runs-on: ubuntu-latest
if: github.event_name == 'push'

permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v2

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Github container registry
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.event_name == 'release' }}

- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/transdb-de/backend/transdb-backend:latest
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: mode=max
124 changes: 8 additions & 116 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,117 +1,9 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Config file
config.json

# Exclude IDE's
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
/transdb-migration/*
!/transdb-migration/migrate.js
.idea
.vs
.vscode

# Compiled Javascript

dist/
files/
*.DotSettings.user
49 changes: 49 additions & 0 deletions Attributes/ValidateCaptchaAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using transdb_backend_net.Exceptions;
using transdb_backend_net.Models.Config;
using transdb_backend_net.Services;

namespace transdb_backend_net.Attributes;

/// <summary>
/// Action filter that verifies the Cap CAPTCHA token from the X-Cap-Token request header
/// before allowing the action to execute. Verification is skipped when CAPTCHA is disabled in config.
/// Apply to public endpoints that should be protected against automated submissions.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ValidateCaptchaAttribute : Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var config = context.HttpContext.RequestServices
.GetRequiredService<IOptions<CaptchaConfig>>().Value;

if (!config.Enabled)
{
await next();
return;
}

var token = context.HttpContext.Request.Headers["X-Cap-Token"].FirstOrDefault();

if (string.IsNullOrEmpty(token))
{
context.Result = new CaptchaVerificationError();
return;
}

var captcha = context.HttpContext.RequestServices.GetRequiredService<ICaptchaService>();

var valid = await captcha.VerifyAsync(token);

if (!valid)
{
context.Result = new CaptchaVerificationError();
return;
}

await next();
}
}
60 changes: 60 additions & 0 deletions Controllers/ActivityController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Bson;
using System.Security.Claims;
using System.Text.Json;
using transdb_backend_net.Exceptions;
using transdb_backend_net.Models.Database;
using transdb_backend_net.Models.Request;
using transdb_backend_net.Models.Response;
using transdb_backend_net.Services;

namespace transdb_backend_net.Controllers;

[ApiController]
[Route("activities")]
[Authorize]
public class ActivityController(IEntryActivityService activityService, IEntryService entryService, IDatabaseService databaseService) : ControllerBase
{
/// <summary>Returns a paginated list of all activity events across all entries, enriched with entry names.</summary>
[HttpGet]
[Authorize(Policy = "AdminOnly")]
public async Task<ActionResult<PaginatedResponse<EntryActivityResponse>>> GetAll([FromQuery] int page = 0)
{
var activities = await activityService.GetAllAsync(page);
return Ok(activities);
}

/// <summary>Returns a paginated list of activity events for a specific entry.</summary>
[HttpGet("entry/{id}")]
public async Task<ActionResult<PaginatedResponse<EntryActivity>>> GetByEntry(ObjectId id, [FromQuery] int page = 0)
{
var activities = await activityService.GetByEntryAsync(id, page);
return Ok(activities);
}

/// <summary>
/// Reverts the effect of an activity.
/// Supported: DuplicateDetected (removes duplicate link), Deleted (restores entry with original ID),
/// Blocked/Unblocked (toggles blocked status), Archived (unarchives entry).
/// </summary>
[HttpPost("{activityId}/revert")]
[Authorize(Policy = "AdminOnly")]
public async Task<IActionResult> RevertActivity(ObjectId activityId, [FromBody] CommentedRequest request)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)!;

var activity = await databaseService.GetActivityByIdAsync(activityId);
if (activity == null) return new NotFoundApiError("activity not found");

var result = await activityService.RevertAsync(activity, userId, request.Comment);
if (result.IsFailed)
{
return result.FailureDetails?.Contains("not found") == true
? new NotFoundApiError(result.FailureDetails)
: new InvalidRequestApiError(result.FailureDetails);
}

return Ok();
}
}
Loading