i skibidi a few toilets

This commit is contained in:
fISHIE
2026-01-28 15:46:44 +05:00
parent 327dadff22
commit be7239fde7
7 changed files with 363 additions and 56 deletions
@@ -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<Author> _authorRepository;
private readonly IMongoRepository<PetitionDetail> _petitionRepository;
public readonly TurnstileService _turnstileService;
public PetitionController(
IOptions<PetitionSettings> petitionSettings,
IMongoRepository<Author> authorRepository,
IMongoRepository<PetitionDetail> 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<IActionResult> 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("\"", "\\\"");
}
}
}
+3
View File
@@ -28,5 +28,8 @@ namespace Submission.Api.Dto
[Required] [Required]
public string PetitionBodyEng { get; set; } = string.Empty; public string PetitionBodyEng { get; set; } = string.Empty;
[Required]
public string turnstileToken { get; set; } = string.Empty;
} }
} }
+11
View File
@@ -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; }
}
}
+1 -1
View File
@@ -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" 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" rel="stylesheet"
/> />
<title>frontend-react</title> <title>MvDevsUnion</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+23 -1
View File
@@ -3,6 +3,26 @@ import type { PetitionDetails, SignatureSubmission } from "@/types/petition";
// API base URL - empty for same-origin requests through Vite proxy // API base URL - empty for same-origin requests through Vite proxy
const API_BASE_URL = ""; const API_BASE_URL = "";
export interface SimplePetition {
id: string;
slug: string;
title: string;
title_Dhiv: string;
signatureCount: number;
}
export async function fetchLatestPetitions(): Promise<SimplePetition[]> {
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( export async function fetchPetition(
petitionId: string, petitionId: string,
): Promise<PetitionDetails> { ): Promise<PetitionDetails> {
@@ -40,6 +60,7 @@ export interface PetitionFormData {
authorNid: string; authorNid: string;
petitionBodyDhiv: string; petitionBodyDhiv: string;
petitionBodyEng: string; petitionBodyEng: string;
turnstileToken: string;
} }
export interface SubmitPetitionResponse { export interface SubmitPetitionResponse {
@@ -63,9 +84,10 @@ export async function submitPetition(
formData.append("AuthorNid", data.authorNid); formData.append("AuthorNid", data.authorNid);
formData.append("PetitionBodyDhiv", data.petitionBodyDhiv); formData.append("PetitionBodyDhiv", data.petitionBodyDhiv);
formData.append("PetitionBodyEng", data.petitionBodyEng); formData.append("PetitionBodyEng", data.petitionBodyEng);
formData.append("turnstileToken", data.turnstileToken);
const response = await fetch( const response = await fetch(
`${API_BASE_URL}/api/Debug/upload-petition-form`, `${API_BASE_URL}/api/Petition/upload-petition-form`,
{ {
method: "POST", method: "POST",
body: formData, body: formData,
+49 -22
View File
@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState, useRef } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { submitPetition, type PetitionFormData } from "@/lib/api"; import { submitPetition, type PetitionFormData } from "@/lib/api";
import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile";
import { import {
FileText, FileText,
Send, Send,
@@ -104,8 +105,10 @@ export function CreatePetitionPage() {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<{ slug: string } | null>(null); const [success, setSuccess] = useState<{ slug: string } | null>(null);
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
const turnstileRef = useRef<TurnstileInstance>(null);
const [formData, setFormData] = useState<PetitionFormData>({ const [formData, setFormData] = useState<Omit<PetitionFormData, "turnstileToken">>({
slug: "", slug: "",
nameDhiv: "", nameDhiv: "",
nameEng: "", nameEng: "",
@@ -142,14 +145,27 @@ export function CreatePetitionPage() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true);
setError(null); setError(null);
// Validate turnstile token
if (!turnstileToken && !import.meta.env.DEV) {
setError("Please complete the verification challenge.");
return;
}
setIsSubmitting(true);
try { try {
const result = await submitPetition(formData); const result = await submitPetition({
...formData,
turnstileToken: turnstileToken || "DEV_BYPASS_TOKEN",
});
setSuccess({ slug: result.slug }); setSuccess({ slug: result.slug });
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to submit petition"); setError(err instanceof Error ? err.message : "Failed to submit petition");
// Reset turnstile on error
turnstileRef.current?.reset();
setTurnstileToken(null);
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@@ -181,6 +197,8 @@ export function CreatePetitionPage() {
<button <button
onClick={() => { onClick={() => {
setSuccess(null); setSuccess(null);
setTurnstileToken(null);
turnstileRef.current?.reset();
setFormData({ setFormData({
slug: "", slug: "",
nameDhiv: "", nameDhiv: "",
@@ -371,24 +389,33 @@ export function CreatePetitionPage() {
/> />
</div> </div>
{/* Submit Button */} {/* Turnstile and Submit */}
<button <div className="flex flex-col items-center gap-4">
type="submit" <Turnstile
disabled={isSubmitting} ref={turnstileRef}
className="w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-3 px-6 rounded-lg transition-colors" siteKey={import.meta.env.VITE_TURNSTILE_SITEKEY}
> onSuccess={setTurnstileToken}
{isSubmitting ? ( onError={() => setTurnstileToken(null)}
<> onExpire={() => setTurnstileToken(null)}
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" /> />
Creating... <button
</> type="submit"
) : ( disabled={isSubmitting || (!turnstileToken && !import.meta.env.DEV)}
<> className="w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-3 px-6 rounded-lg transition-colors"
<Send className="w-5 h-5" /> >
Create Petition {isSubmitting ? (
</> <>
)} <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
</button> Creating...
</>
) : (
<>
<Send className="w-5 h-5" />
Create Petition
</>
)}
</button>
</div>
</form> </form>
</div> </div>
+96 -32
View File
@@ -1,48 +1,112 @@
import { useState, useEffect } from "react";
import { Link } from "react-router-dom"; 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() { export function HomePage() {
const [petitions, setPetitions] = useState<SimplePetition[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 ( return (
<div className="min-h-screen bg-slate-50/50 p-4 md:p-8 font-sans"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100 font-sans">
<div className="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-slate-100 p-6 md:p-10 animate-in fade-in duration-500 slide-in-from-bottom-4"> {/* Hero Section */}
<div className="text-center space-y-6"> <div className="bg-white border-b border-slate-100">
<div className="flex justify-center"> <div className="max-w-4xl mx-auto px-4 py-12 md:py-20">
<div className="bg-blue-100 p-4 rounded-full"> <div className="text-center space-y-6">
<FileText className="w-12 h-12 text-blue-600" /> <div className="flex justify-center">
<div className="bg-blue-100 p-4 rounded-full">
<FileText className="w-12 h-12 text-blue-600" />
</div>
</div> </div>
</div>
<h1 className="text-3xl md:text-4xl font-bold text-slate-900"> <h1 className="text-3xl md:text-5xl font-bold text-slate-900">
Petition.com.mv Bringing Real Change
</h1> </h1>
<p className="text-lg text-slate-600 max-w-xl mx-auto"> <p className="text-lg md:text-xl text-slate-600 max-w-2xl mx-auto">
A platform for creating and signing petitions. Make your voice 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. heard on issues that matter to you and your community.
</p>
<div className="bg-slate-50 rounded-lg p-6 mt-8">
<h2 className="text-lg font-semibold text-slate-800 mb-2">
Looking for a petition?
</h2>
<p className="text-slate-600">
Use the direct link shared with you to view and sign a petition.
</p> </p>
</div>
<div className="pt-4"> <div className="pt-4">
<Link <Link
to="/CreatePetition" to="/CreatePetition"
className="inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors" className="inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-8 rounded-lg transition-colors shadow-md hover:shadow-lg"
> >
<PenLine className="w-5 h-5" /> <PenLine className="w-5 h-5" />
Create a Petition Create a Petition
</Link> </Link>
</div>
</div> </div>
</div> </div>
</div> </div>
<footer className="text-center text-slate-500 text-sm mt-6 pb-4"> {/* Latest Petitions Section */}
<div className="max-w-4xl mx-auto px-4 py-12">
<h2 className="text-2xl font-bold text-slate-900 mb-6">
Latest Petitions
</h2>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
</div>
) : error ? (
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
<p className="text-slate-500">{error}</p>
</div>
) : petitions.length === 0 ? (
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
<p className="text-slate-500">No petitions yet. Be the first to create one!</p>
</div>
) : (
<div className="space-y-4">
{petitions.map((petition) => (
<Link
key={petition.id}
to={`/Petition/${petition.slug}`}
className="block bg-white rounded-xl border border-slate-200 p-5 hover:border-blue-300 hover:shadow-md transition-all group"
>
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-slate-900 group-hover:text-blue-600 transition-colors truncate">
{petition.title}
</h3>
<p className="text-sm text-slate-500 mt-1 truncate dhivehi" dir="rtl">
{petition.title_Dhiv}
</p>
</div>
<div className="flex items-center gap-4 flex-shrink-0">
<div className="flex items-center gap-1.5 text-slate-600">
<Users className="w-4 h-4" />
<span className="font-medium">{petition.signatureCount}</span>
</div>
<ChevronRight className="w-5 h-5 text-slate-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
</Link>
))}
</div>
)}
</div>
<footer className="text-center text-slate-500 text-sm py-8">
Powered by Mv Devs Union Powered by Mv Devs Union
</footer> </footer>
</div> </div>