i did that shit

This commit is contained in:
fISHIE
2025-12-14 12:09:05 +05:00
parent cea629affa
commit 0d763ea736
14 changed files with 1087 additions and 71 deletions

BIN
Frontend/fonts/utheem.ttf Normal file

Binary file not shown.

408
Frontend/index.html Normal file
View File

@@ -0,0 +1,408 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Petition Details</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
</head>
<body>
<div class="container">
<div id="loading" class="loading">Loading petition...</div>
<div id="error" class="error" style="display: none;"></div>
<div id="petition-content" style="display: none;">
<div class="lang-switcher">
<button id="lang-en" class="lang-btn active">English</button>
<button id="lang-dv" class="lang-btn">ދިވެހި</button>
</div>
<div class="petition-header">
<h1 id="petition-name-eng" class="lang-en-content"></h1>
<h2 id="petition-name-dhiv" class="dhivehi lang-dv-content"></h2>
<div class="metadata">
<span class="start-date">Start Date: <span id="start-date"></span></span>
<span class="signature-count">Signatures: <span id="signature-count"></span></span>
</div>
</div>
<div class="author-details">
<h3 class="lang-en-content">Author Details</h3>
<h3 class="dhivehi lang-dv-content">ލިޔުންތެރިގެ މައުލޫމާތު</h3>
<p><strong class="lang-en-content">Name:</strong><strong class="dhivehi lang-dv-content">ނަން:</strong> <span id="author-name"></span></p>
<!-- <p><strong class="lang-en-content">NID:</strong><strong class="dhivehi lang-dv-content">ކާޑު ނަންބަރު:</strong> <span id="author-nid"></span></p> -->
</div>
<div class="petition-body">
<div class="lang-en-content">
<div id="petition-body-eng" class="body-content"></div>
</div>
<div class="lang-dv-content">
<div id="petition-body-dhiv" class="body-content dhivehi"></div>
</div>
</div>
<div class="signature-section">
<h3 class="lang-en-content">Sign this Petition</h3>
<h3 class="dhivehi lang-dv-content">މި މައްސަލައިގައި ސޮއި ކުރައްވާ</h3>
<form id="signature-form">
<div class="form-group">
<label class="lang-en-content">Full Name</label>
<label class="dhivehi lang-dv-content">ފުރިހަމަ ނަން</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label class="lang-en-content">ID Card Number</label>
<label class="dhivehi lang-dv-content">ކާޑު ނަންބަރު</label>
<input type="text" id="idCard" name="idCard" required>
</div>
<div class="form-group">
<label class="lang-en-content">Signature</label>
<label class="dhivehi lang-dv-content">ސޮއި</label>
<div class="signature-pad-container">
<canvas id="signature-pad" width="600" height="200"></canvas>
</div>
</div>
<div id="form-message" class="form-message"></div>
<div class="form-buttons">
<button type="button" id="clear-signature" class="btn-secondary" title="Clear signature" aria-label="Clear signature">
<i class="fas fa-eraser"></i> Clear
</button>
<button type="submit" class="btn-primary" title="Submit signature" aria-label="Submit signature">
<i class="fas fa-paper-plane"></i> Submit
</button>
</div>
</form>
</div>
</div>
</div>
<script>
// API Configuration - Change this for different environments
const API_CONFIG = {
baseUrl: 'http://localhost:5299' // Change to production URL when deploying
};
let currentLang = 'en';
// Extract petition ID from URL query parameter
function getPetitionIdFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('id');
}
// Get language from URL query parameter
function getLangFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
const lang = urlParams.get('lang');
return lang === 'dv' ? 'dv' : 'en';
}
// Update URL with current language
function updateUrlWithLang(lang) {
const urlParams = new URLSearchParams(window.location.search);
if (lang === 'en') {
urlParams.delete('lang');
} else {
urlParams.set('lang', lang);
}
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
window.history.pushState({}, '', newUrl);
}
// Switch language
function switchLanguage(lang) {
currentLang = lang;
// Update button states
document.getElementById('lang-en').classList.toggle('active', lang === 'en');
document.getElementById('lang-dv').classList.toggle('active', lang === 'dv');
// Show/hide content based on language
const enContent = document.querySelectorAll('.lang-en-content');
const dvContent = document.querySelectorAll('.lang-dv-content');
enContent.forEach(el => {
el.style.display = lang === 'en' ? '' : 'none';
});
dvContent.forEach(el => {
el.style.display = lang === 'dv' ? '' : 'none';
});
// Update URL
updateUrlWithLang(lang);
}
// Fetch petition data
async function fetchPetition(petitionId) {
const apiUrl = `${API_CONFIG.baseUrl}/api/Sign/petition/${petitionId}`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
displayPetition(data);
} catch (error) {
showError(`Failed to load petition: ${error.message}`);
}
}
// Display petition data
function displayPetition(data) {
document.getElementById('loading').style.display = 'none';
document.getElementById('petition-content').style.display = 'block';
document.getElementById('petition-name-eng').textContent = data.nameEng;
document.getElementById('petition-name-dhiv').textContent = data.nameDhiv;
document.getElementById('start-date').textContent = data.startDate;
document.getElementById('signature-count').textContent = data.signatureCount;
document.getElementById('author-name').textContent = data.authorDetails.name;
//document.getElementById('author-nid').textContent = data.authorDetails.nid;
// Convert markdown-style formatting to HTML (basic support)
document.getElementById('petition-body-eng').innerHTML = formatText(data.petitionBodyEng);
document.getElementById('petition-body-dhiv').innerHTML = formatText(data.petitionBodyDhiv);
// Set initial language display
switchLanguage(currentLang);
}
// Parse Markdown to HTML
function formatText(text) {
return marked.parse(text);
}
// Show error message
function showError(message) {
document.getElementById('loading').style.display = 'none';
const errorDiv = document.getElementById('error');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
// Signature pad functionality
let isDrawing = false;
let signaturePaths = [];
let currentPath = [];
function initSignaturePad() {
const canvas = document.getElementById('signature-pad');
const ctx = canvas.getContext('2d');
// Set up canvas style
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Mouse events
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
// Touch events
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', stopDrawing);
// Clear button
document.getElementById('clear-signature').addEventListener('click', clearSignature);
}
function getMousePos(canvas, evt) {
const rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
function getTouchPos(canvas, evt) {
const rect = canvas.getBoundingClientRect();
return {
x: evt.touches[0].clientX - rect.left,
y: evt.touches[0].clientY - rect.top
};
}
function startDrawing(e) {
isDrawing = true;
const canvas = document.getElementById('signature-pad');
const pos = getMousePos(canvas, e);
currentPath = [pos];
}
function draw(e) {
if (!isDrawing) return;
const canvas = document.getElementById('signature-pad');
const ctx = canvas.getContext('2d');
const pos = getMousePos(canvas, e);
currentPath.push(pos);
ctx.beginPath();
ctx.moveTo(currentPath[currentPath.length - 2].x, currentPath[currentPath.length - 2].y);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
}
function stopDrawing() {
if (isDrawing && currentPath.length > 0) {
signaturePaths.push([...currentPath]);
currentPath = [];
}
isDrawing = false;
}
function handleTouchStart(e) {
e.preventDefault();
isDrawing = true;
const canvas = document.getElementById('signature-pad');
const pos = getTouchPos(canvas, e);
currentPath = [pos];
}
function handleTouchMove(e) {
e.preventDefault();
if (!isDrawing) return;
const canvas = document.getElementById('signature-pad');
const ctx = canvas.getContext('2d');
const pos = getTouchPos(canvas, e);
currentPath.push(pos);
ctx.beginPath();
ctx.moveTo(currentPath[currentPath.length - 2].x, currentPath[currentPath.length - 2].y);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
}
function clearSignature() {
const canvas = document.getElementById('signature-pad');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
signaturePaths = [];
currentPath = [];
}
function generateSVG() {
if (signaturePaths.length === 0) {
return null;
}
let pathData = '';
signaturePaths.forEach(path => {
if (path.length > 0) {
pathData += `M ${path[0].x} ${path[0].y} `;
for (let i = 1; i < path.length; i++) {
pathData += `L ${path[i].x} ${path[i].y} `;
}
}
});
const svg = `<svg width="600" height="200" xmlns="http://www.w3.org/2000/svg"><path d="${pathData}" stroke="black" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
return svg;
}
// Form submission
async function submitSignature(e) {
e.preventDefault();
const name = document.getElementById('name').value;
const idCard = document.getElementById('idCard').value;
const signature = generateSVG();
if (!signature) {
showFormMessage('Please provide your signature.', 'error');
return;
}
const petitionId = getPetitionIdFromUrl();
const apiUrl = `${API_CONFIG.baseUrl}/api/Sign/petition/${petitionId}`;
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'accept': '*/*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
idCard: idCard,
signature: signature
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
showFormMessage('Signature submitted successfully!', 'success');
document.getElementById('signature-form').reset();
clearSignature();
// Refresh petition data to update signature count
setTimeout(() => {
fetchPetition(petitionId);
}, 1000);
} catch (error) {
showFormMessage(`Failed to submit signature: ${error.message}`, 'error');
}
}
function showFormMessage(message, type) {
const messageDiv = document.getElementById('form-message');
messageDiv.textContent = message;
messageDiv.className = `form-message ${type}`;
messageDiv.style.display = 'block';
setTimeout(() => {
messageDiv.style.display = 'none';
}, 5000);
}
// Initialize on page load
window.addEventListener('DOMContentLoaded', () => {
const petitionId = getPetitionIdFromUrl();
if (!petitionId) {
showError('No petition ID found in URL. Please provide a valid petition URL.');
return;
}
// Get initial language from URL
currentLang = getLangFromUrl();
// Set up language switcher event listeners
document.getElementById('lang-en').addEventListener('click', () => switchLanguage('en'));
document.getElementById('lang-dv').addEventListener('click', () => switchLanguage('dv'));
// Initialize signature pad
initSignaturePad();
// Set up form submission
document.getElementById('signature-form').addEventListener('submit', submitSignature);
fetchPetition(petitionId);
});
</script>
</body>
</html>

334
Frontend/style.css Normal file
View File

@@ -0,0 +1,334 @@
@font-face {
font-family: 'Utheem';
src: url('fonts/utheem.woff') format('woff'),
url('fonts/utheem.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Shangu';
src: url('fonts/shangu.woff') format('woff'),
url('fonts/shangu.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 40px;
}
.lang-switcher {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-bottom: 20px;
}
.lang-btn {
padding: 8px 20px;
border: 2px solid #007bff;
background-color: white;
color: #007bff;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.lang-btn:hover {
background-color: #f0f8ff;
}
.lang-btn.active {
background-color: #007bff;
color: white;
}
.lang-btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
.loading {
text-align: center;
font-size: 18px;
color: #666;
padding: 40px;
}
.error {
background-color: #fee;
border: 1px solid #fcc;
color: #c33;
padding: 20px;
border-radius: 4px;
margin-bottom: 20px;
}
.petition-header {
border-bottom: 2px solid #007bff;
padding-bottom: 20px;
margin-bottom: 30px;
}
.petition-header h1 {
font-size: 32px;
color: #222;
margin-bottom: 10px;
}
.petition-header h2 {
font-size: 24px;
color: #555;
margin-bottom: 15px;
}
.petition-header h2.dhivehi {
font-family: 'Shangu', 'Faruma', 'MV Faseyha', 'Waheed', 'Noto Sans Thaana', sans-serif;
}
.dhivehi {
font-family: 'Utheem', 'Faruma', 'MV Faseyha', 'Waheed', 'Noto Sans Thaana', sans-serif;
direction: rtl;
text-align: right;
}
.metadata {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-top: 15px;
font-size: 14px;
color: #666;
}
.metadata span {
background-color: #f0f0f0;
padding: 6px 12px;
border-radius: 4px;
}
.metadata span span {
background-color: transparent;
padding: 0;
font-weight: bold;
color: #333;
}
.author-details {
background-color: #f9f9f9;
padding: 20px;
border-radius: 6px;
margin-bottom: 30px;
}
.author-details h3 {
font-size: 18px;
color: #007bff;
margin-bottom: 12px;
}
.author-details p {
margin-bottom: 8px;
}
.petition-body {
margin-top: 30px;
}
.petition-body h3 {
font-size: 20px;
color: #007bff;
margin-top: 30px;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
}
.body-content {
padding: 15px;
background-color: #fafafa;
border-radius: 4px;
line-height: 1.8;
white-space: pre-wrap;
}
.body-content strong {
color: #222;
}
.signature-count {
font-weight: bold;
}
.signature-count span {
color: #007bff !important;
}
.signature-section {
margin-top: 40px;
padding-top: 30px;
border-top: 2px solid #e0e0e0;
}
.signature-section h3 {
font-size: 24px;
color: #007bff;
margin-bottom: 25px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
.form-group input[type="text"] {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-group input[type="text"]:focus {
outline: none;
border-color: #007bff;
}
.signature-pad-container {
border: 2px solid #ddd;
border-radius: 6px;
background-color: white;
display: inline-block;
cursor: crosshair;
margin-bottom: 10px;
}
#signature-pad {
display: block;
touch-action: none;
}
.signature-actions {
margin-bottom: 15px;
}
.form-buttons {
display: flex;
gap: 12px;
align-items: center;
}
.btn-primary,
.btn-secondary {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.form-message {
padding: 12px;
border-radius: 6px;
margin-bottom: 15px;
display: none;
}
.form-message.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.form-message.error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.petition-header h1 {
font-size: 24px;
}
.petition-header h2 {
font-size: 18px;
}
.metadata {
flex-direction: column;
gap: 10px;
}
#signature-pad {
width: 100%;
max-width: 100%;
}
.signature-pad-container {
width: 100%;
overflow-x: auto;
}
.form-buttons {
flex-direction: column;
width: 100%;
}
.form-buttons button {
width: 100%;
}
}

View File

@@ -1,7 +1,10 @@
# WPetition Submission API
a self hostable e petition system to collect signatures for your cause.
## why make this
maldives parliment promised the release of a e-petition system powered by efass will be released months ago and then never released it
i said fuck it i want data protection bill so i made this simple signature collection system since the law doesnt care if youre signature is signed digitally or via wet ink.
made it in 5 hours and didnt even vibe code it
## nerd shit
A petition signing API built with ASP.NET Core 9.0 that allows users to sign petitions and retrieve petition details. Features rate limiting to prevent spam and duplicate signature detection.
@@ -26,11 +29,7 @@ A petition signing API built with ASP.NET Core 9.0 that allows users to sign pet
### MongoDB Setup
1. Create a MongoDB database for the petition system
2. Create the following collections:
- `signatures` - stores petition signatures
- `petitions` - stores petition details
- `authors` - stores petition author information
refer to the details below
### Application Configuration
@@ -38,6 +37,9 @@ Update `appsettings.json` with your MongoDB connection settings:
```json
{
"PetitionSettings": {
"AllowPetitionCreation": true
},
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "petition_database"
@@ -52,6 +54,10 @@ Update `appsettings.json` with your MongoDB connection settings:
}
```
by default `AllowPetitionCreation` is true. you must upload your Petition to the debug controller and then shut down the server and set this value to false and reboot or anyone will be able to submit petitions.
check out `sample.Petition.md` on how to structure your petition so it will be accepted by the server
### Rate Limiting Configuration
The API is configured with rate limiting to prevent spam. Default settings in `Program.cs`:
@@ -197,41 +203,6 @@ Retrieves details of a specific petition including author information.
{}
```
## Data Models
### Signature (Widget)
```csharp
{
"id": "ObjectId",
"name": "string",
"idCard": "string",
"signature_SVG": "string",
"timestamp": "DateTime"
}
```
### Petition Details
```csharp
{
"id": "Guid",
"startDate": "DateOnly",
"nameDhiv": "string",
"nameEng": "string",
"petitionBodyDhiv": "string",
"petitionBodyEng": "string",
"authorId": "Guid",
"signatureCount": "int"
}
```
### Author
```csharp
{
"id": "Guid",
"name": "string",
"nid": "string"
}
```
## Security Features

View File

@@ -0,0 +1,6 @@
namespace Submission.Api.Configuration;
public class PetitionSettings
{
public bool AllowPetitionCreation { get; set; }
}

View File

@@ -0,0 +1,241 @@
using Ashi.MongoInterface.Service;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Submission.Api.Configuration;
using Submission.Api.Models;
using System.Globalization;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace Submission.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class DebugController : ControllerBase
{
private readonly PetitionSettings _petitionSettings;
private readonly IMongoRepository<Author> _authorRepository;
private readonly IMongoRepository<PetitionDetail> _petitionRepository;
public DebugController(
IOptions<PetitionSettings> petitionSettings,
IMongoRepository<Author> authorRepository,
IMongoRepository<PetitionDetail> petitionRepository)
{
_petitionSettings = petitionSettings.Value;
_authorRepository = authorRepository;
_petitionRepository = petitionRepository;
}
[HttpGet("petitions", Name = "GetPetitions")]
public IActionResult GetPetitions()
{
try
{
var files = Directory.EnumerateFiles("Petitions");
return Ok(files);
}
catch (Exception e)
{
return Problem("Petitions Folder not found");
}
}
[HttpGet("create-petition-folder", Name = "CreatePetitionFolder")]
public IActionResult create_petition_folder()
{
try
{
Directory.CreateDirectory("Petitions");
return Ok("Petitions folder created");
}
catch (Exception e)
{
return Problem(e.Message);
}
}
[HttpPost("upload-petition", Name = "UploadPetition")]
public async Task<IActionResult> UploadPetition(IFormFile file)
{
// 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" });
}
// Validate file exists
if (file == null || file.Length == 0)
{
return BadRequest(new { message = "No file uploaded" });
}
// Validate file extension
if (!file.FileName.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
{
return BadRequest(new { message = "Only .md files are allowed" });
}
try
{
// Read file content
string fileContent;
using (var reader = new StreamReader(file.OpenReadStream()))
{
fileContent = await reader.ReadToEndAsync();
}
// Parse frontmatter and body
var (frontmatter, body) = ParseMarkdownFile(fileContent);
if (frontmatter == null)
{
return BadRequest(new { message = "Invalid markdown format. Frontmatter is required." });
}
// Parse YAML frontmatter
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
var metadata = deserializer.Deserialize<Dictionary<string, object>>(frontmatter);
// Extract values
var petitionId = Guid.NewGuid();
var startDateStr = metadata["startDate"].ToString();
var nameDhiv = metadata["nameDhiv"].ToString();
var nameEng = metadata["nameEng"].ToString();
var authorData = metadata["author"] as Dictionary<object, object>;
var authorName = authorData["name"].ToString();
var authorNid = authorData["nid"].ToString();
// Parse start date (format: dd-MM-yyyy)
var startDate = DateOnly.ParseExact(startDateStr, "dd-MM-yyyy", CultureInfo.InvariantCulture);
// Parse petition bodies from markdown
var (petitionBodyDhiv, petitionBodyEng) = ParsePetitionBodies(body);
// Check if petition already exists
var existingPetition = await _petitionRepository.FindByIdAsync(petitionId);
if (existingPetition != null)
{
return Conflict(new { message = $"A petition with ID '{petitionId}' already exists in the database" });
}
// Create or get author
var author = await _authorRepository.FindOneAsync(x => x.NID == authorNid);
if (author == null)
{
author = new Author
{
Id = Guid.NewGuid(),
Name = authorName,
NID = authorNid
};
await _authorRepository.InsertOneAsync(author);
}
// Create petition
var petition = new PetitionDetail
{
Id = petitionId,
StartDate = startDate,
NameDhiv = nameDhiv,
NameEng = nameEng,
AuthorId = author.Id,
PetitionBodyDhiv = petitionBodyDhiv,
PetitionBodyEng = petitionBodyEng,
SignatureCount = 0
};
await _petitionRepository.InsertOneAsync(petition);
// Save file with GUID prefix
Directory.CreateDirectory("Petitions");
var newFileName = $"{Guid.NewGuid()}_{file.FileName}";
var filePath = Path.Combine("Petitions", newFileName);
await System.IO.File.WriteAllTextAsync(filePath, fileContent);
return Ok(new
{
message = "Petition created successfully",
petitionId = petitionId,
fileName = newFileName,
filePath = filePath,
authorId = author.Id
});
}
catch (Exception e)
{
return Problem(e.Message);
}
}
private (string frontmatter, string body) ParseMarkdownFile(string content)
{
var lines = content.Split('\n');
if (lines.Length < 3 || lines[0].Trim() != "---")
{
return (null, null);
}
var frontmatterLines = new List<string>();
var bodyLines = new List<string>();
var inFrontmatter = true;
var frontmatterClosed = false;
for (int i = 1; i < lines.Length; i++)
{
if (lines[i].Trim() == "---" && inFrontmatter)
{
inFrontmatter = false;
frontmatterClosed = true;
continue;
}
if (inFrontmatter)
{
frontmatterLines.Add(lines[i]);
}
else
{
bodyLines.Add(lines[i]);
}
}
if (!frontmatterClosed)
{
return (null, null);
}
return (string.Join("\n", frontmatterLines), string.Join("\n", bodyLines));
}
private (string dhivehiBody, string englishBody) ParsePetitionBodies(string body)
{
var dhivehiBody = "";
var englishBody = "";
var sections = body.Split("##", StringSplitOptions.RemoveEmptyEntries);
foreach (var section in sections)
{
var trimmed = section.Trim();
if (trimmed.StartsWith("Petition Body (Dhivehi)", StringComparison.OrdinalIgnoreCase))
{
dhivehiBody = trimmed.Replace("Petition Body (Dhivehi)", "").Trim();
}
else if (trimmed.StartsWith("Petition Body (English)", StringComparison.OrdinalIgnoreCase))
{
englishBody = trimmed.Replace("Petition Body (English)", "").Trim();
}
}
return (dhivehiBody, englishBody);
}
}
}

View File

@@ -1,10 +1,8 @@
using System.CodeDom;
using System.Runtime.InteropServices;
using Ashi.MongoInterface.Service;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Caching.Memory;
using MongoDB.Driver;
using Submission.Api.Dto;
using Submission.Api.Models;
@@ -14,16 +12,16 @@ namespace Submission.Api.Controllers
[ApiController]
public class SignController : ControllerBase
{
private readonly IMongoRepository<Author> _authorRepository;
private readonly IMongoRepository<PetitionDetail> _detailRepository;
private readonly IMongoRepository<Widget> _signatureRepository;
private readonly IMongoRepository<Signature> _signatureRepository;
private readonly IMemoryCache _cache;
public SignController(
IMongoRepository<Author> authorRepository,
IMongoRepository<PetitionDetail> detailRepository,
IMongoRepository<Widget> signatureRepository,
IMongoRepository<Signature> signatureRepository,
IMemoryCache cache)
{
_authorRepository = authorRepository;
@@ -32,32 +30,53 @@ namespace Submission.Api.Controllers
_cache = cache;
}
[HttpPost(Name = "petition/{id}")]
[HttpPost("petition/{petition_id}", Name = "SignPetition")]
[EnableRateLimiting("SignPetitionPolicy")]
public async Task<IActionResult> SignDisHoe([FromRoute]Guid petition_id,[FromBody] WidgetsDto body)
public async Task<IActionResult> SignDisHoe([FromRoute] Guid petition_id, [FromBody] WidgetsDto body)
{
var cacheKey = $"petition_{petition_id}";
var pet = await _detailRepository.FindByIdAsync(petition_id);
if (pet == null)
return NotFound();
//TODO : add svg validation
//check to see if the same person signed the petition already
//if dupe send error saying user already signed
var dupe = await _signatureRepository.FindOneAsync(x => x.IdCard == body.IdCard);
if (dupe != null)
return Problem("You already signed this petition");
var dupe = await _signatureRepository.FindOneAsync(x => x.IdCard == body.IdCard);
if (dupe != null)
return Problem("You already signed this petition");
//add signature to the db
await _signatureRepository.InsertOneAsync(new Widget
await _signatureRepository.InsertOneAsync(new Signature
{
IdCard = body.IdCard,
Name = body.Name,
Signature_SVG = body.Signature,
Timestamp = DateTime.Now
Timestamp = DateTime.Now,
PetitionId = petition_id
});
//update signature count
if (pet.SignatureCount == null)
{
pet.SignatureCount = 0;
}
var count_update_filter = Builders<PetitionDetail>.Filter.Eq("_id", petition_id);
var Countupdate = Builders<PetitionDetail>.Update.Inc("SignatureCount", 1);
await _detailRepository.UpdateOneAsync(count_update_filter, Countupdate);
_cache.Remove(cacheKey);
return Ok("your signature has been submitted");
}
[HttpGet(Name = "petition/{id}")]
public async Task<IActionResult> GetDisHoe([FromRoute] Guid petition_id)
[HttpGet("petition/{petition_id}", Name = "GetPetition")]
public async Task<IActionResult> GetDisHoe([FromRoute] Guid petition_id)
{
var cacheKey = $"petition_{petition_id}";
@@ -68,7 +87,7 @@ namespace Submission.Api.Controllers
}
// Not in cache, fetch from database
var pet = await _detailRepository.FindByIdAsync(petition_id);
var pet = await _detailRepository.FindByIdAsync(petition_id);
if (pet == null)
return NotFound();
@@ -88,12 +107,14 @@ namespace Submission.Api.Controllers
{
Name = author.Name,
NID = author.NID,
}
},
SignatureCount = pet.SignatureCount
};
// Store in cache with 5 minute expiration
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(12));
.SetAbsoluteExpiration(TimeSpan.FromHours(1));
_cache.Set(cacheKey, dto, cacheOptions);

View File

@@ -3,10 +3,12 @@
namespace Submission.Api.Models;
[BsonCollection("signatures")]
public class Widget : Document
public class Signature : Document
{
public string Name { get; set; }
public string IdCard { get; set; }
public string Signature_SVG { get; set; }
public DateTime Timestamp { get; set; }
public Guid PetitionId { get; set; }
}

View File

@@ -1,13 +1,14 @@
using System.Configuration;
using Ashi.MongoInterface;
using Ashi.MongoInterface.Service;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Submission.Api.Configuration;
var builder = WebApplication.CreateBuilder(args);
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.Configure<MongoDbSettings>(builder.Configuration.GetSection("MongoDbSettings"));
builder.Services.Configure<PetitionSettings>(builder.Configuration.GetSection("PetitionSettings"));
builder.Services.AddSingleton<IMongoDbSettings>(serviceProvider =>
serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value);
@@ -17,8 +18,9 @@ builder.Services.AddScoped((typeof(IMongoRepository<>)), typeof(MongoRepository<
builder.Services.AddMemoryCache();
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
// Add Swagger/OpenAPI
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add rate limiting
builder.Services.AddRateLimiter(options =>
@@ -37,7 +39,8 @@ var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();

View File

@@ -4,7 +4,7 @@
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchBrowser": true,
"applicationUrl": "http://localhost:5299",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"

View File

@@ -8,8 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11"/>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.11" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="YamlDotNet" Version="16.2.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
<ActiveDebugProfile>http</ActiveDebugProfile>
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>

View File

@@ -1,4 +1,11 @@
{
"PetitionSettings": {
"AllowPetitionCreation": true
},
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "petition_database"
},
"Logging": {
"LogLevel": {
"Default": "Information",

17
sample.Petition.md Normal file
View File

@@ -0,0 +1,17 @@
---
startDate: 14-12-2025
nameDhiv: "މިސާލު ނަން"
nameEng: "Sample Petition Name"
author:
name: "Fishie"
nid: "AAAAA12345"
---
## Petition Body (Dhivehi)
މިއީ ދިވެހި ބަސްނުވަތައް ލިޔެވިފައިވާ ބައިތައް.
## Petition Body (English)
This is the English version of the petition body written in clear paragraphs.
You can use normal Markdown formatting here such as **bold**, lists, and links.