added slug to make the url nicer

moved cf to its own file to keep code clean
This commit is contained in:
fISHIE
2026-01-28 14:58:02 +05:00
parent ae5f449889
commit 31b2929927
6 changed files with 144 additions and 79 deletions

View File

@@ -217,6 +217,13 @@ namespace Submission.Api.Controllers
return BadRequest(new { message = "StartDate must be in format dd-MM-yyyy" });
}
// Check for duplicate slug
var existingPetition = await _petitionRepository.FindOneAsync(x => x.Slug == form.Slug);
if (existingPetition != null)
{
return Conflict(new { message = $"A petition with slug '{form.Slug}' already exists" });
}
var petitionId = Guid.NewGuid();
// Create or get author
@@ -236,6 +243,7 @@ namespace Submission.Api.Controllers
var petition = new PetitionDetail
{
Id = petitionId,
Slug = form.Slug,
StartDate = startDate,
NameDhiv = form.NameDhiv,
NameEng = form.NameEng,
@@ -248,7 +256,7 @@ namespace Submission.Api.Controllers
await _petitionRepository.InsertOneAsync(petition);
// Build markdown file content and save to Petitions folder
var frontmatter = $"---\nstartDate: {form.StartDate}\nnameDhiv: \"{EscapeYaml(form.NameDhiv)}\"\nnameEng: \"{EscapeYaml(form.NameEng)}\"\nauthor:\n name: \"{EscapeYaml(form.AuthorName)}\"\n nid: \"{EscapeYaml(form.AuthorNid)}\"\n---\n";
var frontmatter = $"---\nslug: \"{EscapeYaml(form.Slug)}\"\nstartDate: {form.StartDate}\nnameDhiv: \"{EscapeYaml(form.NameDhiv)}\"\nnameEng: \"{EscapeYaml(form.NameEng)}\"\nauthor:\n name: \"{EscapeYaml(form.AuthorName)}\"\n nid: \"{EscapeYaml(form.AuthorNid)}\"\n---\n";
var body = $"## Petition Body (Dhivehi)\n\n{form.PetitionBodyDhiv}\n\n## Petition Body (English)\n\n{form.PetitionBodyEng}\n";
var fileContent = frontmatter + "\n" + body;
@@ -262,6 +270,7 @@ namespace Submission.Api.Controllers
{
message = "Petition created successfully",
petitionId = petitionId,
slug = form.Slug,
fileName = newFileName,
filePath = filePath,
authorId = author.Id

View File

@@ -2,12 +2,10 @@ using Ashi.MongoInterface.Service;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using Submission.Api.Dto;
using Submission.Api.Models;
using System.Text.Json;
using System.Text.Json.Serialization;
using Submission.Api.Services;
namespace Submission.Api.Controllers
{
@@ -129,6 +127,7 @@ namespace Submission.Api.Controllers
var dto = new PetitionDetailsDto
{
Id = petition_id,
Slug = pet.Slug,
NameDhiv = pet.NameDhiv,
StartDate = pet.StartDate,
NameEng = pet.NameEng,
@@ -152,92 +151,52 @@ namespace Submission.Api.Controllers
return Ok(dto);
}
}
#region Turnstile Service
public class TurnstileSettings
{
public string SecretKey { get; set; } = string.Empty;
}
public class TurnstileService
{
private readonly HttpClient _httpClient;
private readonly string _secretKey;
private readonly IWebHostEnvironment _env;
private const string SiteverifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
public TurnstileService(HttpClient httpClient, IOptions<TurnstileSettings> options, IWebHostEnvironment env)
[HttpGet("petition/by-slug/{slug}", Name = "GetPetitionBySlug")]
public async Task<IActionResult> GetPetitionBySlug([FromRoute] string slug)
{
_httpClient = httpClient;
_secretKey = options?.Value?.SecretKey ?? throw new ArgumentNullException(nameof(options), "Turnstile:SecretKey must be configured in appsettings.json");
_env = env;
}
var cacheKey = $"petition_slug_{slug}";
public async Task<TurnstileResponse> ValidateTokenAsync(string token, string remoteip = null)
{
if (_env.IsDevelopment() && token == "DEV_BYPASS_TOKEN")
// Try to get from cache
if (_cache.TryGetValue(cacheKey, out PetitionDetailsDto cachedDto))
{
return new TurnstileResponse { Success = true };
return Ok(cachedDto);
}
var parameters = new Dictionary<string, string>
// Not in cache, fetch from database by slug
var pet = await _detailRepository.FindOneAsync(x => x.Slug == slug);
if (pet == null)
return NotFound();
var author = await _authorRepository.FindOneAsync(x => x.Id == pet.AuthorId);
var dto = new PetitionDetailsDto
{
{ "secret", _secretKey },
{ "response", token }
Id = pet.Id,
Slug = pet.Slug,
NameDhiv = pet.NameDhiv,
StartDate = pet.StartDate,
NameEng = pet.NameEng,
PetitionBodyDhiv = pet.PetitionBodyDhiv,
PetitionBodyEng = pet.PetitionBodyEng,
AuthorDetails = new AuthorsDto
{
Name = author.Name,
NID = author.NID,
},
SignatureCount = pet.SignatureCount
};
if (!string.IsNullOrEmpty(remoteip))
{
parameters.Add("remoteip", remoteip);
}
// Store in cache with 1 hour expiration
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(1));
var postContent = new FormUrlEncodedContent(parameters);
_cache.Set(cacheKey, dto, cacheOptions);
try
{
var response = await _httpClient.PostAsync(SiteverifyUrl, postContent);
var stringContent = await response.Content.ReadAsStringAsync();
Console.WriteLine("Turnstile response: " + stringContent);
// deserialize with case-insensitive option; mapping for "error-codes" is handled by attribute on ErrorCodes
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
return JsonSerializer.Deserialize<TurnstileResponse>(stringContent, options);
}
catch (Exception)
{
return new TurnstileResponse
{
Success = false,
ErrorCodes = new[] { "internal-error" }
};
}
return Ok(dto);
}
}
public class TurnstileResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
// Cloudflare returns "error-codes" (with a hyphen) — map it explicitly
[JsonPropertyName("error-codes")]
public string[] ErrorCodes { get; set; }
[JsonPropertyName("challenge_ts")]
public string ChallengeTs { get; set; }
public string Hostname { get; set; }
public string Action { get; set; }
// "cdata" may be present
public string Cdata { get; set; }
// metadata is optional and can be an object
public JsonElement? Metadata { get; set; }
}
#endregion
}

View File

@@ -3,6 +3,7 @@
public class PetitionDetailsDto
{
public Guid Id { get; set; }
public string Slug { get; set; }
public DateOnly StartDate { get; set; }
public string NameDhiv { get; set; }

View File

@@ -4,6 +4,9 @@ namespace Submission.Api.Dto
{
public class PetitionFormDto
{
[Required]
public string Slug { get; set; } = string.Empty;
[Required]
public string NameDhiv { get; set; } = string.Empty;

View File

@@ -6,6 +6,7 @@ namespace Submission.Api.Models;
[BsonCollection("petitionDetail")]
public class PetitionDetail : Document
{
public string Slug { get; set; }
public DateOnly StartDate { get; set; }
public string NameDhiv { get; set; }
@@ -17,4 +18,6 @@ public class PetitionDetail : Document
public string PetitionBodyEng { get; set; }
public int SignatureCount { get; set; }
public bool isApproved { get; set; } = false;
}

View File

@@ -0,0 +1,90 @@
using Microsoft.Extensions.Options;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Submission.Api.Services
{
public class TurnstileSettings
{
public string SecretKey { get; set; } = string.Empty;
}
public class TurnstileService
{
private readonly HttpClient _httpClient;
private readonly string _secretKey;
private readonly IWebHostEnvironment _env;
private const string SiteverifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
public TurnstileService(HttpClient httpClient, IOptions<TurnstileSettings> options, IWebHostEnvironment env)
{
_httpClient = httpClient;
_secretKey = options?.Value?.SecretKey ?? throw new ArgumentNullException(nameof(options), "Turnstile:SecretKey must be configured in appsettings.json");
_env = env;
}
public async Task<TurnstileResponse> ValidateTokenAsync(string token, string remoteip = null)
{
if (_env.IsDevelopment() && token == "DEV_BYPASS_TOKEN")
{
return new TurnstileResponse { Success = true };
}
var parameters = new Dictionary<string, string>
{
{ "secret", _secretKey },
{ "response", token }
};
if (!string.IsNullOrEmpty(remoteip))
{
parameters.Add("remoteip", remoteip);
}
var postContent = new FormUrlEncodedContent(parameters);
try
{
var response = await _httpClient.PostAsync(SiteverifyUrl, postContent);
var stringContent = await response.Content.ReadAsStringAsync();
Console.WriteLine("Turnstile response: " + stringContent);
// deserialize with case-insensitive option; mapping for "error-codes" is handled by attribute on ErrorCodes
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
return JsonSerializer.Deserialize<TurnstileResponse>(stringContent, options);
}
catch (Exception)
{
return new TurnstileResponse
{
Success = false,
ErrorCodes = new[] { "internal-error" }
};
}
}
}
public class TurnstileResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
// Cloudflare returns "error-codes" (with a hyphen) — map it explicitly
[JsonPropertyName("error-codes")]
public string[] ErrorCodes { get; set; }
[JsonPropertyName("challenge_ts")]
public string ChallengeTs { get; set; }
public string Hostname { get; set; }
public string Action { get; set; }
// "cdata" may be present
public string Cdata { get; set; }
// metadata is optional and can be an object
public JsonElement? Metadata { get; set; }
}
}