From be7239fde7cee8e04b653fe4a5cbe5f7c9a51654 Mon Sep 17 00:00:00 2001 From: fISHIE <83373559+WhoIsFishie@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:46:44 +0500 Subject: [PATCH] i skibidi a few toilets --- .../Controllers/PetitionController.cs | 180 ++++++++++++++++++ Submission.Api/Dto/PetitionFormDto.cs | 3 + Submission.Api/Dto/SimplePetitionDto.cs | 11 ++ frontend-react/index.html | 2 +- frontend-react/src/lib/api.ts | 24 ++- .../src/pages/CreatePetitionPage.tsx | 71 ++++--- frontend-react/src/pages/HomePage.tsx | 128 +++++++++---- 7 files changed, 363 insertions(+), 56 deletions(-) create mode 100644 Submission.Api/Controllers/PetitionController.cs create mode 100644 Submission.Api/Dto/SimplePetitionDto.cs diff --git a/Submission.Api/Controllers/PetitionController.cs b/Submission.Api/Controllers/PetitionController.cs new file mode 100644 index 0000000..a02119d --- /dev/null +++ b/Submission.Api/Controllers/PetitionController.cs @@ -0,0 +1,180 @@ +using Ashi.MongoInterface.Service; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Submission.Api.Configuration; +using Submission.Api.Dto; +using Submission.Api.Models; +using Submission.Api.Services; +using System.Globalization; + +namespace Submission.Api.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class PetitionController : ControllerBase + { + private readonly PetitionSettings _petitionSettings; + private readonly IMongoRepository _authorRepository; + private readonly IMongoRepository _petitionRepository; + public readonly TurnstileService _turnstileService; + + public PetitionController( + IOptions petitionSettings, + IMongoRepository authorRepository, + IMongoRepository petitionRepository, + TurnstileService turnstileService) + { + _petitionSettings = petitionSettings.Value; + _authorRepository = authorRepository; + _petitionRepository = petitionRepository; + _turnstileService = turnstileService; + + } + + + // New endpoint: form-based petition upload + [HttpPost("upload-petition-form", Name = "UploadPetitionForm")] + public async Task UploadPetitionForm([FromForm] PetitionFormDto form) + { + var remoteip = HttpContext.Request.Headers["CF-Connecting-IP"].FirstOrDefault() ?? + HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault() ?? + HttpContext.Connection.RemoteIpAddress?.ToString(); + + if (form.turnstileToken == null) + return BadRequest("Turnstile token is missing"); + + Console.WriteLine("Token received: " + form.turnstileToken); + + var validation = await _turnstileService.ValidateTokenAsync(form.turnstileToken, remoteip); + + if(!validation.Success) + { + // Invalid token - reject submission + // Make joining error codes null-safe to avoid ArgumentNullException + var errorCodes = validation?.ErrorCodes; + var errors = (errorCodes != null && errorCodes.Length > 0) + ? string.Join(", ", errorCodes) + : "unknown"; + return BadRequest($"Verification failed: {errors}"); + } + + // Check if petition creation is allowed + if (!_petitionSettings.AllowPetitionCreation) + { + return StatusCode(403, new { message = "Petition creation is disabled. Set 'PetitionSettings:AllowPetitionCreation' to true in appsettings.json" }); + } + + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + // Parse start date (format: dd-MM-yyyy) + DateOnly startDate; + try + { + startDate = DateOnly.ParseExact(form.StartDate, "dd-MM-yyyy", CultureInfo.InvariantCulture); + } + catch (FormatException) + { + 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 + var author = await _authorRepository.FindOneAsync(x => x.NID == form.AuthorNid); + if (author == null) + { + author = new Author + { + Id = Guid.NewGuid(), + Name = form.AuthorName, + NID = form.AuthorNid + }; + await _authorRepository.InsertOneAsync(author); + } + + // Create petition + var petition = new PetitionDetail + { + Id = petitionId, + Slug = form.Slug, + StartDate = startDate, + NameDhiv = form.NameDhiv, + NameEng = form.NameEng, + AuthorId = author.Id, + PetitionBodyDhiv = form.PetitionBodyDhiv, + PetitionBodyEng = form.PetitionBodyEng, + SignatureCount = 0, + isApproved = false + }; + + await _petitionRepository.InsertOneAsync(petition); + + // Build markdown file content and save to Petitions folder + 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; + + Directory.CreateDirectory("Petitions"); + var newFileName = $"{Guid.NewGuid()}.md"; + var filePath = Path.Combine("Petitions", newFileName); + + await System.IO.File.WriteAllTextAsync(filePath, fileContent); + + return Ok(new + { + message = "Petition created successfully", + petitionId = petitionId, + slug = form.Slug, + fileName = newFileName, + filePath = filePath, + authorId = author.Id + }); + } + catch (Exception e) + { + return Problem(e.Message); + } + } + + + [HttpGet("get-latest-petitions", Name = "GetLatestPetitions")] + public IActionResult GetLatestPetitions() + { + var latestPetitions = _petitionRepository + .FilterBy(p => p.isApproved == true) + .OrderByDescending(p => p.StartDate) + .Take(10) + .ToList(); + + var dtoList = latestPetitions.Select(p => new SimplePetitionDto + { + Id = p.Id, + Slug = p.Slug, + Title = p.NameEng, + Title_Dhiv = p.NameDhiv, + SignatureCount = p.SignatureCount + }).ToList(); + + return Ok(dtoList); + } + + private static string EscapeYaml(string value) + { + if (string.IsNullOrEmpty(value)) return ""; + return value.Replace("\"", "\\\""); + } + } +} diff --git a/Submission.Api/Dto/PetitionFormDto.cs b/Submission.Api/Dto/PetitionFormDto.cs index fdf27a9..4b8f7f9 100644 --- a/Submission.Api/Dto/PetitionFormDto.cs +++ b/Submission.Api/Dto/PetitionFormDto.cs @@ -28,5 +28,8 @@ namespace Submission.Api.Dto [Required] public string PetitionBodyEng { get; set; } = string.Empty; + + [Required] + public string turnstileToken { get; set; } = string.Empty; } } diff --git a/Submission.Api/Dto/SimplePetitionDto.cs b/Submission.Api/Dto/SimplePetitionDto.cs new file mode 100644 index 0000000..19ab5fb --- /dev/null +++ b/Submission.Api/Dto/SimplePetitionDto.cs @@ -0,0 +1,11 @@ +namespace Submission.Api.Dto +{ + public class SimplePetitionDto + { + public string Title { get; set; } + public string Title_Dhiv { get; set; } + public int SignatureCount{get; set;} + public Guid Id { get; set; } + public string Slug { get; set; } + } +} diff --git a/frontend-react/index.html b/frontend-react/index.html index d58c843..f4d7375 100644 --- a/frontend-react/index.html +++ b/frontend-react/index.html @@ -10,7 +10,7 @@ href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet" /> - frontend-react + MvDevsUnion
diff --git a/frontend-react/src/lib/api.ts b/frontend-react/src/lib/api.ts index 2d206c5..e2c7bb9 100644 --- a/frontend-react/src/lib/api.ts +++ b/frontend-react/src/lib/api.ts @@ -3,6 +3,26 @@ import type { PetitionDetails, SignatureSubmission } from "@/types/petition"; // API base URL - empty for same-origin requests through Vite proxy const API_BASE_URL = ""; +export interface SimplePetition { + id: string; + slug: string; + title: string; + title_Dhiv: string; + signatureCount: number; +} + +export async function fetchLatestPetitions(): Promise { + const response = await fetch( + `${API_BASE_URL}/api/Petition/get-latest-petitions`, + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); +} + export async function fetchPetition( petitionId: string, ): Promise { @@ -40,6 +60,7 @@ export interface PetitionFormData { authorNid: string; petitionBodyDhiv: string; petitionBodyEng: string; + turnstileToken: string; } export interface SubmitPetitionResponse { @@ -63,9 +84,10 @@ export async function submitPetition( formData.append("AuthorNid", data.authorNid); formData.append("PetitionBodyDhiv", data.petitionBodyDhiv); formData.append("PetitionBodyEng", data.petitionBodyEng); + formData.append("turnstileToken", data.turnstileToken); const response = await fetch( - `${API_BASE_URL}/api/Debug/upload-petition-form`, + `${API_BASE_URL}/api/Petition/upload-petition-form`, { method: "POST", body: formData, diff --git a/frontend-react/src/pages/CreatePetitionPage.tsx b/frontend-react/src/pages/CreatePetitionPage.tsx index a762f76..0b084d6 100644 --- a/frontend-react/src/pages/CreatePetitionPage.tsx +++ b/frontend-react/src/pages/CreatePetitionPage.tsx @@ -1,6 +1,7 @@ -import { useState } from "react"; +import { useState, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { submitPetition, type PetitionFormData } from "@/lib/api"; +import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile"; import { FileText, Send, @@ -104,8 +105,10 @@ export function CreatePetitionPage() { const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState<{ slug: string } | null>(null); + const [turnstileToken, setTurnstileToken] = useState(null); + const turnstileRef = useRef(null); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState>({ slug: "", nameDhiv: "", nameEng: "", @@ -142,14 +145,27 @@ export function CreatePetitionPage() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setIsSubmitting(true); setError(null); + // Validate turnstile token + if (!turnstileToken && !import.meta.env.DEV) { + setError("Please complete the verification challenge."); + return; + } + + setIsSubmitting(true); + try { - const result = await submitPetition(formData); + const result = await submitPetition({ + ...formData, + turnstileToken: turnstileToken || "DEV_BYPASS_TOKEN", + }); setSuccess({ slug: result.slug }); } catch (err) { setError(err instanceof Error ? err.message : "Failed to submit petition"); + // Reset turnstile on error + turnstileRef.current?.reset(); + setTurnstileToken(null); } finally { setIsSubmitting(false); } @@ -181,6 +197,8 @@ export function CreatePetitionPage() { + {/* Turnstile and Submit */} +
+ setTurnstileToken(null)} + onExpire={() => setTurnstileToken(null)} + /> + +
diff --git a/frontend-react/src/pages/HomePage.tsx b/frontend-react/src/pages/HomePage.tsx index c6731b8..fc08444 100644 --- a/frontend-react/src/pages/HomePage.tsx +++ b/frontend-react/src/pages/HomePage.tsx @@ -1,48 +1,112 @@ +import { useState, useEffect } from "react"; import { Link } from "react-router-dom"; -import { FileText, PenLine } from "lucide-react"; +import { fetchLatestPetitions, type SimplePetition } from "@/lib/api"; +import { FileText, PenLine, Users, ChevronRight, Loader2 } from "lucide-react"; export function HomePage() { + const [petitions, setPetitions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function loadPetitions() { + try { + const data = await fetchLatestPetitions(); + setPetitions(data); + } catch (err) { + console.warn("Failed to fetch petitions:", err); + setError("Could not load petitions"); + } finally { + setLoading(false); + } + } + loadPetitions(); + }, []); + return ( -
-
-
-
-
- +
+ {/* Hero Section */} +
+
+
+
+
+ +
-
-

- Petition.com.mv -

+

+ Bringing Real Change +

-

- A platform for creating and signing petitions. Make your voice - heard on issues that matter to you and your community. -

- -
-

- Looking for a petition? -

-

- Use the direct link shared with you to view and sign a petition. +

+ A platform for creating and signing petitions. With 0 costs to the tax payer and no Efass. Make your voice + heard on issues that matter to you and your community.

-
-
- - - Create a Petition - +
+ + + Create a Petition + +
-