mirror of
https://github.com/MvDevsUnion/WPetition.git
synced 2026-04-29 19:43:22 +00:00
added slug to make the url nicer
moved cf to its own file to keep code clean
This commit is contained in:
@@ -217,6 +217,13 @@ namespace Submission.Api.Controllers
|
|||||||
return BadRequest(new { message = "StartDate must be in format dd-MM-yyyy" });
|
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();
|
var petitionId = Guid.NewGuid();
|
||||||
|
|
||||||
// Create or get author
|
// Create or get author
|
||||||
@@ -236,6 +243,7 @@ namespace Submission.Api.Controllers
|
|||||||
var petition = new PetitionDetail
|
var petition = new PetitionDetail
|
||||||
{
|
{
|
||||||
Id = petitionId,
|
Id = petitionId,
|
||||||
|
Slug = form.Slug,
|
||||||
StartDate = startDate,
|
StartDate = startDate,
|
||||||
NameDhiv = form.NameDhiv,
|
NameDhiv = form.NameDhiv,
|
||||||
NameEng = form.NameEng,
|
NameEng = form.NameEng,
|
||||||
@@ -248,7 +256,7 @@ namespace Submission.Api.Controllers
|
|||||||
await _petitionRepository.InsertOneAsync(petition);
|
await _petitionRepository.InsertOneAsync(petition);
|
||||||
|
|
||||||
// Build markdown file content and save to Petitions folder
|
// 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 body = $"## Petition Body (Dhivehi)\n\n{form.PetitionBodyDhiv}\n\n## Petition Body (English)\n\n{form.PetitionBodyEng}\n";
|
||||||
var fileContent = frontmatter + "\n" + body;
|
var fileContent = frontmatter + "\n" + body;
|
||||||
|
|
||||||
@@ -262,6 +270,7 @@ namespace Submission.Api.Controllers
|
|||||||
{
|
{
|
||||||
message = "Petition created successfully",
|
message = "Petition created successfully",
|
||||||
petitionId = petitionId,
|
petitionId = petitionId,
|
||||||
|
slug = form.Slug,
|
||||||
fileName = newFileName,
|
fileName = newFileName,
|
||||||
filePath = filePath,
|
filePath = filePath,
|
||||||
authorId = author.Id
|
authorId = author.Id
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ using Ashi.MongoInterface.Service;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using Submission.Api.Dto;
|
using Submission.Api.Dto;
|
||||||
using Submission.Api.Models;
|
using Submission.Api.Models;
|
||||||
using System.Text.Json;
|
using Submission.Api.Services;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Submission.Api.Controllers
|
namespace Submission.Api.Controllers
|
||||||
{
|
{
|
||||||
@@ -129,6 +127,7 @@ namespace Submission.Api.Controllers
|
|||||||
var dto = new PetitionDetailsDto
|
var dto = new PetitionDetailsDto
|
||||||
{
|
{
|
||||||
Id = petition_id,
|
Id = petition_id,
|
||||||
|
Slug = pet.Slug,
|
||||||
NameDhiv = pet.NameDhiv,
|
NameDhiv = pet.NameDhiv,
|
||||||
StartDate = pet.StartDate,
|
StartDate = pet.StartDate,
|
||||||
NameEng = pet.NameEng,
|
NameEng = pet.NameEng,
|
||||||
@@ -152,92 +151,52 @@ namespace Submission.Api.Controllers
|
|||||||
|
|
||||||
return Ok(dto);
|
return Ok(dto);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
[HttpGet("petition/by-slug/{slug}", Name = "GetPetitionBySlug")]
|
||||||
#region Turnstile Service
|
public async Task<IActionResult> GetPetitionBySlug([FromRoute] string slug)
|
||||||
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;
|
var cacheKey = $"petition_slug_{slug}";
|
||||||
_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)
|
// Try to get from cache
|
||||||
{
|
if (_cache.TryGetValue(cacheKey, out PetitionDetailsDto cachedDto))
|
||||||
if (_env.IsDevelopment() && token == "DEV_BYPASS_TOKEN")
|
|
||||||
{
|
{
|
||||||
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 },
|
Id = pet.Id,
|
||||||
{ "response", token }
|
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))
|
// Store in cache with 1 hour expiration
|
||||||
{
|
var cacheOptions = new MemoryCacheEntryOptions()
|
||||||
parameters.Add("remoteip", remoteip);
|
.SetAbsoluteExpiration(TimeSpan.FromHours(1));
|
||||||
}
|
|
||||||
|
|
||||||
var postContent = new FormUrlEncodedContent(parameters);
|
_cache.Set(cacheKey, dto, cacheOptions);
|
||||||
|
|
||||||
try
|
return Ok(dto);
|
||||||
{
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
public class PetitionDetailsDto
|
public class PetitionDetailsDto
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
public string Slug { get; set; }
|
||||||
public DateOnly StartDate { get; set; }
|
public DateOnly StartDate { get; set; }
|
||||||
|
|
||||||
public string NameDhiv { get; set; }
|
public string NameDhiv { get; set; }
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ namespace Submission.Api.Dto
|
|||||||
{
|
{
|
||||||
public class PetitionFormDto
|
public class PetitionFormDto
|
||||||
{
|
{
|
||||||
|
[Required]
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public string NameDhiv { get; set; } = string.Empty;
|
public string NameDhiv { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace Submission.Api.Models;
|
|||||||
[BsonCollection("petitionDetail")]
|
[BsonCollection("petitionDetail")]
|
||||||
public class PetitionDetail : Document
|
public class PetitionDetail : Document
|
||||||
{
|
{
|
||||||
|
public string Slug { get; set; }
|
||||||
public DateOnly StartDate { get; set; }
|
public DateOnly StartDate { get; set; }
|
||||||
|
|
||||||
public string NameDhiv { get; set; }
|
public string NameDhiv { get; set; }
|
||||||
@@ -17,4 +18,6 @@ public class PetitionDetail : Document
|
|||||||
public string PetitionBodyEng { get; set; }
|
public string PetitionBodyEng { get; set; }
|
||||||
|
|
||||||
public int SignatureCount { get; set; }
|
public int SignatureCount { get; set; }
|
||||||
|
|
||||||
|
public bool isApproved { get; set; } = false;
|
||||||
}
|
}
|
||||||
90
Submission.Api/Services/TurnstileService.cs
Normal file
90
Submission.Api/Services/TurnstileService.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user