From 31b29299270a98d8aaf6dce74e7ada8854f95fb1 Mon Sep 17 00:00:00 2001 From: fISHIE <83373559+WhoIsFishie@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:58:02 +0500 Subject: [PATCH] added slug to make the url nicer moved cf to its own file to keep code clean --- Submission.Api/Controllers/DebugController.cs | 11 +- Submission.Api/Controllers/SignController.cs | 115 ++++++------------ Submission.Api/Dto/PetitionDetailsDto.cs | 1 + Submission.Api/Dto/PetitionFormDto.cs | 3 + Submission.Api/Models/PetitionDetail.cs | 3 + Submission.Api/Services/TurnstileService.cs | 90 ++++++++++++++ 6 files changed, 144 insertions(+), 79 deletions(-) create mode 100644 Submission.Api/Services/TurnstileService.cs diff --git a/Submission.Api/Controllers/DebugController.cs b/Submission.Api/Controllers/DebugController.cs index bcf6427..b6ae977 100644 --- a/Submission.Api/Controllers/DebugController.cs +++ b/Submission.Api/Controllers/DebugController.cs @@ -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 diff --git a/Submission.Api/Controllers/SignController.cs b/Submission.Api/Controllers/SignController.cs index b608557..16e311d 100644 --- a/Submission.Api/Controllers/SignController.cs +++ b/Submission.Api/Controllers/SignController.cs @@ -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 options, IWebHostEnvironment env) + [HttpGet("petition/by-slug/{slug}", Name = "GetPetitionBySlug")] + public async Task 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 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 + // 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(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 } diff --git a/Submission.Api/Dto/PetitionDetailsDto.cs b/Submission.Api/Dto/PetitionDetailsDto.cs index bfe266c..f2a0d1f 100644 --- a/Submission.Api/Dto/PetitionDetailsDto.cs +++ b/Submission.Api/Dto/PetitionDetailsDto.cs @@ -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; } diff --git a/Submission.Api/Dto/PetitionFormDto.cs b/Submission.Api/Dto/PetitionFormDto.cs index 203f49d..fdf27a9 100644 --- a/Submission.Api/Dto/PetitionFormDto.cs +++ b/Submission.Api/Dto/PetitionFormDto.cs @@ -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; diff --git a/Submission.Api/Models/PetitionDetail.cs b/Submission.Api/Models/PetitionDetail.cs index 31351ae..6c1c475 100644 --- a/Submission.Api/Models/PetitionDetail.cs +++ b/Submission.Api/Models/PetitionDetail.cs @@ -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; } \ No newline at end of file diff --git a/Submission.Api/Services/TurnstileService.cs b/Submission.Api/Services/TurnstileService.cs new file mode 100644 index 0000000..13e4fd7 --- /dev/null +++ b/Submission.Api/Services/TurnstileService.cs @@ -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 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 ValidateTokenAsync(string token, string remoteip = null) + { + if (_env.IsDevelopment() && token == "DEV_BYPASS_TOKEN") + { + return new TurnstileResponse { Success = true }; + } + + var parameters = new Dictionary + { + { "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(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; } + } +}