Implement Turnstile captcha for petition signing and add tweet prompt modal

This commit is contained in:
fISHIE
2025-12-17 11:40:38 +05:00
parent 973868336d
commit 1e44b19a70
14 changed files with 940 additions and 49 deletions

View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(docker compose build:*)",
"Bash(docker compose:*)"
]
}
}

298
DOCKER_README.md Normal file
View File

@@ -0,0 +1,298 @@
# Docker Setup Guide
## Overview
This project uses Docker Compose to run both the frontend and API in containers with Nginx as a unified gateway.
### Architecture
```
┌─────────────────────────────────────────┐
│ Browser (localhost:8080) │
└────────────────┬────────────────────────┘
┌─────────────────────────────────────────┐
│ Nginx Container │
│ - Serves static frontend files │
│ - Proxies /api/* to backend │
│ - Proxies /swagger to backend │
└────────────────┬────────────────────────┘
┌─────────────────────────────────────────┐
│ .NET API Container │
│ - ASP.NET Core 9.0 │
│ - Connects to MongoDB on host │
└─────────────────────────────────────────┘
```
## Prerequisites
- Docker Desktop installed and running
- MongoDB running on your host machine (port 27017)
- Port 9755 available on your host
## Quick Start
### 1. Build and Start Containers
```bash
docker compose build
docker compose up -d
```
### 2. Access the Application
- **Frontend**: http://localhost:9755
- **API** (via proxy): http://localhost:9755/api/*
- **Swagger UI**: http://localhost:9755/swagger
### 3. View Logs
```bash
# View all logs
docker compose logs -f
# View specific service logs
docker compose logs -f nginx
docker compose logs -f submission.api
```
### 4. Stop Containers
```bash
docker compose down
```
## Making Changes
### Frontend Changes (HTML/CSS/JS)
1. Edit files in the `Frontend/` directory
2. Rebuild the nginx container:
```bash
docker compose build nginx && docker compose up -d
```
### API Changes (.NET/C# Code)
1. Edit files in the `Submission.Api/` directory
2. Rebuild the API container:
```bash
docker compose build submission.api && docker compose up -d
```
### Both Frontend and API Changed
```bash
docker compose build && docker compose up -d
```
## Common Commands
### Container Management
```bash
# Start containers
docker compose up -d
# Stop containers
docker compose down
# Restart containers
docker compose restart
# View running containers
docker compose ps
# Remove all containers and volumes
docker compose down -v
```
### Logs and Debugging
```bash
# Follow all logs
docker compose logs -f
# View last 50 lines of API logs
docker compose logs --tail=50 submission.api
# View last 50 lines of Nginx logs
docker compose logs --tail=50 nginx
# Execute command inside API container
docker compose exec submission.api /bin/bash
```
### Rebuilding
```bash
# Rebuild all containers
docker compose build
# Rebuild with no cache (clean build)
docker compose build --no-cache
# Rebuild specific service
docker compose build nginx
docker compose build submission.api
```
## Configuration
### MongoDB Connection
The API connects to MongoDB on your host machine using `host.docker.internal:27017`.
To change the MongoDB connection:
1. Edit `compose.yaml`
2. Update the `MongoDbSettings__ConnectionString` environment variable
3. Restart containers
```yaml
environment:
- MongoDbSettings__ConnectionString=mongodb://host.docker.internal:27017
```
### Port Configuration
The application is exposed on port 9755. To change this:
1. Edit `compose.yaml`
2. Update the nginx ports mapping:
```yaml
ports:
- "9755:80" # Change 9755 to your desired port
```
3. Restart containers
### API Base URL (Frontend)
The frontend API configuration is in `Frontend/index.html` at line 105:
```javascript
const API_CONFIG = {
baseUrl: '' // Empty for same-origin requests
};
```
This is set to empty because Nginx proxies all `/api/*` requests to the backend.
## File Structure
```
.
├── compose.yaml # Docker Compose configuration
├── nginx/
│ ├── Dockerfile # Nginx container definition
│ └── nginx.conf # Nginx configuration
├── Frontend/ # Static frontend files
│ ├── index.html
│ ├── style.css
│ └── fonts/
└── Submission.Api/
├── Dockerfile # API container definition
└── ... # .NET source files
```
## Troubleshooting
### MongoDB Connection Issues
**Problem**: API can't connect to MongoDB on host
**Solution**:
1. Verify MongoDB is running on host: `mongosh --eval "db.version()"`
2. Check MongoDB is listening on `0.0.0.0:27017` not just `127.0.0.1`
3. Check Windows Firewall isn't blocking the connection
4. Verify the connection string in `compose.yaml` uses `host.docker.internal`
### Port 9755 Already in Use
**Problem**: Error binding to port 9755
**Solution**:
1. Find what's using the port: `netstat -ano | findstr :9755`
2. Either stop that process or change the port in `compose.yaml`
### Container Won't Start
**Problem**: Container crashes on startup
**Solution**:
```bash
# Check logs for errors
docker compose logs submission.api
# Rebuild with no cache
docker compose build --no-cache
# Remove old containers and rebuild
docker compose down
docker compose up -d
```
### Changes Not Reflected
**Problem**: Code changes don't appear after rebuild
**Solution**:
```bash
# Force rebuild and restart
docker compose down
docker compose build --no-cache
docker compose up -d
```
### Nginx 502 Bad Gateway
**Problem**: Nginx can't reach the API
**Solution**:
1. Check API container is running: `docker compose ps`
2. Check API logs: `docker compose logs submission.api`
3. Verify API is listening on port 8080
4. Check both containers are on the same network
## Production Deployment
For production deployment:
1. **Update API base URL**: Change port 8080 to 80 (or use a reverse proxy)
2. **MongoDB**: Use a proper MongoDB connection string with authentication
3. **HTTPS**: Add SSL certificates to Nginx configuration
4. **Environment variables**: Use `.env` file for sensitive configuration
5. **Logging**: Configure proper log aggregation
6. **Health checks**: Add Docker health checks to compose.yaml
### Example Production Changes
```yaml
# compose.yaml
services:
nginx:
ports:
- "80:80"
- "443:443"
volumes:
- ./ssl:/etc/nginx/ssl:ro
submission.api:
environment:
- ASPNETCORE_ENVIRONMENT=Production
- MongoDbSettings__ConnectionString=${MONGODB_CONNECTION_STRING}
```
## Additional Resources
- [Docker Compose Documentation](https://docs.docker.com/compose/)
- [Nginx Documentation](https://nginx.org/en/docs/)
- [ASP.NET Core in Docker](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/)
## Support
For issues or questions:
1. Check container logs: `docker compose logs -f`
2. Verify all services are running: `docker compose ps`
3. Review this documentation
4. Check Docker Desktop is running and healthy

View File

@@ -7,6 +7,12 @@
<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>
<link rel="preconnect" href="https://challenges.cloudflare.com">
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer></script>
</head>
<body>
<div class="container">
@@ -69,7 +75,13 @@
</div>
</div>
<div id="form-message" class="form-message"></div>
<div id="form-message" class="form-message"></div>
<!-- Cloudflare Turnstile: add callbacks so auto-render uses our handlers -->
<div class="cf-turnstile" data-sitekey="0x4AAAAAACHH4QC3wIhkCuhd"
data-callback="onTurnstileSuccess"
data-error-callback="onTurnstileError"
data-expired-callback="onTurnstileExpired"></div>
<div class="form-buttons">
<button type="button" id="clear-signature" class="btn-secondary" title="Clear signature" aria-label="Clear signature">
@@ -84,10 +96,25 @@
</div>
</div>
<!-- Tweet Prompt Modal -->
<div id="tweet-modal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h2 id="modal-title">Share this petition!</h2>
<p id="modal-text">Help spread the word by tweeting about this petition.</p>
<div class="modal-buttons">
<button id="tweet-button" class="btn-primary">
<i class="fab fa-twitter"></i> <span id="tweet-btn-text">Tweet Now</span>
</button>
<button id="skip-tweet" class="btn-secondary"><span id="skip-btn-text">Maybe Later</span></button>
</div>
</div>
</div>
<script>
// API Configuration - Change this for different environments
const API_CONFIG = {
baseUrl: 'http://localhost:5299' // Change to production URL when deploying
baseUrl: '' // Empty for same-origin requests (Nginx proxies /api/* to backend)
};
let currentLang = 'en';
@@ -161,6 +188,9 @@
// Display petition data
function displayPetition(data) {
// Store petition data for tweet generation
currentPetitionData = data;
document.getElementById('loading').style.display = 'none';
document.getElementById('petition-content').style.display = 'block';
@@ -197,6 +227,49 @@
let isDrawing = false;
let signaturePaths = [];
let currentPath = [];
// Turnstile widget state
let turnstileWidgetId = null;
let turnstileToken = null;
function onTurnstileSuccess(token) {
turnstileToken = token;
}
function onTurnstileError() {
turnstileToken = null;
}
function onTurnstileExpired() {
turnstileToken = null;
}
// Try to render Turnstile widget when API is available
function ensureTurnstileRendered() {
const container = document.querySelector('.cf-turnstile');
if (!container) return;
// If Turnstile already auto-rendered an iframe inside the container, don't render again
if (container.children.length > 0) {
// mark as auto-rendered
turnstileWidgetId = -1;
return;
}
if (window.turnstile && turnstileWidgetId === null) {
try {
turnstileWidgetId = turnstile.render(container, {
sitekey: container.getAttribute('data-sitekey'),
callback: onTurnstileSuccess,
'error-callback': onTurnstileError,
'expired-callback': onTurnstileExpired
});
} catch (e) {
// If render fails, leave the element as-is (script may auto-render)
}
} else if (!window.turnstile) {
// Retry until turnstile script is available
setTimeout(ensureTurnstileRendered, 200);
}
}
function initSignaturePad() {
const canvas = document.getElementById('signature-pad');
@@ -336,6 +409,55 @@
const petitionId = getPetitionIdFromUrl();
const apiUrl = `${API_CONFIG.baseUrl}/api/Sign/petition/${petitionId}`;
// Ensure Turnstile widget is rendered
ensureTurnstileRendered();
// Obtain token: prefer the callback value, fall back to getResponse or execute
let token = turnstileToken;
if ((!token || token.length === 0) && window.turnstile && turnstileWidgetId !== null) {
try {
if (typeof turnstile.getResponse === 'function') {
let resp = null;
try {
if (turnstileWidgetId !== null && turnstileWidgetId >= 0) {
resp = turnstile.getResponse(turnstileWidgetId);
} else {
// try without id (auto-rendered widgets may be retrievable this way)
resp = turnstile.getResponse();
}
} catch (e) {
resp = null;
}
if (resp) token = resp;
}
if ((!token || token.length === 0) && typeof turnstile.execute === 'function') {
// Trigger challenge; wait up to ~5s for callback to set token
try { turnstile.execute(turnstileWidgetId); } catch (err) { }
token = await new Promise((resolve) => {
let attempts = 0;
const interval = setInterval(() => {
attempts++;
if (turnstileToken) {
clearInterval(interval);
resolve(turnstileToken);
} else if (attempts > 25) { // ~5 seconds
clearInterval(interval);
resolve(null);
}
}, 200);
});
}
} catch (err) {
token = null;
}
}
if (!token) {
showFormMessage('Please complete the captcha challenge.', 'error');
return;
}
try {
const response = await fetch(apiUrl, {
method: 'POST',
@@ -346,7 +468,8 @@
body: JSON.stringify({
name: name,
idCard: idCard,
signature: signature
signature: signature,
turnstileToken: token
})
});
@@ -355,6 +478,10 @@
}
showFormMessage('Signature submitted successfully!', 'success');
// Show tweet prompt modal
showTweetModal();
document.getElementById('signature-form').reset();
clearSignature();
@@ -365,6 +492,14 @@
} catch (error) {
showFormMessage(`Failed to submit signature: ${error.message}`, 'error');
} finally {
// Reset the Turnstile widget so a fresh token is required next time
try {
if (window.turnstile && turnstileWidgetId !== null) {
turnstile.reset(turnstileWidgetId);
}
} catch (e) { }
turnstileToken = null;
}
}
@@ -379,6 +514,84 @@
}, 5000);
}
// Tweet Modal Functions
let currentPetitionData = null;
function updateModalLanguage() {
const translations = {
en: {
title: 'Share this petition!',
text: 'Help spread the word by tweeting about this petition.',
tweetBtn: 'Tweet Now',
skipBtn: 'Maybe Later'
},
dv: {
title: 'Share this petition!',
text: 'Help spread the word by tweeting about this petition.',
tweetBtn: 'Tweet Now',
skipBtn: 'Maybe Later'
}
};
const t = translations[currentLang];
document.getElementById('modal-title').textContent = t.title;
document.getElementById('modal-text').textContent = t.text;
document.getElementById('tweet-btn-text').textContent = t.tweetBtn;
document.getElementById('skip-btn-text').textContent = t.skipBtn;
}
function showTweetModal() {
updateModalLanguage();
const modal = document.getElementById('tweet-modal');
modal.classList.add('show');
}
function hideTweetModal() {
const modal = document.getElementById('tweet-modal');
modal.classList.remove('show');
}
function generateTweetText() {
if (!currentPetitionData) return '';
const petitionName = currentLang === 'dv' ? currentPetitionData.nameDhiv : currentPetitionData.nameEng;
const petitionUrl = `${window.location.origin}${window.location.pathname}?id=${currentPetitionData.id}`;
return `I just signed "${petitionName}"! \n\nAdd your signature: ${petitionUrl}`;
}
function openTwitterIntent() {
const tweetText = generateTweetText();
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`;
console.log(twitterUrl);
window.open(twitterUrl, '_blank');
hideTweetModal();
}
function setupTweetModalListeners() {
const modal = document.getElementById('tweet-modal');
const tweetButton = document.getElementById('tweet-button');
const skipButton = document.getElementById('skip-tweet');
const closeButton = document.querySelector('.close-modal');
// Tweet button click
tweetButton.addEventListener('click', openTwitterIntent);
// Skip button click
skipButton.addEventListener('click', hideTweetModal);
// Close X click
closeButton.addEventListener('click', hideTweetModal);
// Click outside modal
modal.addEventListener('click', (e) => {
if (e.target === modal) {
hideTweetModal();
}
});
}
// Initialize on page load
window.addEventListener('DOMContentLoaded', () => {
const petitionId = getPetitionIdFromUrl();
@@ -397,10 +610,15 @@
// Initialize signature pad
initSignaturePad();
// Render Turnstile (retry until script available)
ensureTurnstileRendered();
// Set up form submission
document.getElementById('signature-form').addEventListener('submit', submitSignature);
// Set up tweet modal listeners
setupTweetModalListeners();
fetchPetition(petitionId);
});
</script>

View File

@@ -332,3 +332,121 @@ body {
width: 100%;
}
}
/* Tweet Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.3s ease;
}
.modal.show {
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
position: relative;
animation: slideIn 0.3s ease;
}
.close-modal {
position: absolute;
top: 15px;
right: 20px;
font-size: 28px;
font-weight: bold;
color: #999;
cursor: pointer;
transition: color 0.3s ease;
}
.close-modal:hover {
color: #333;
}
.modal-content h2 {
font-size: 24px;
color: #222;
margin-bottom: 15px;
}
.modal-content p {
font-size: 16px;
color: #666;
margin-bottom: 25px;
line-height: 1.6;
}
.modal-buttons {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
width: 100%;
}
.modal-buttons button {
width: 100%;
max-width: 300px;
justify-content: center;
}
.modal-buttons .btn-primary {
background-color: #1DA1F2;
display: flex;
align-items: center;
gap: 8px;
}
.modal-buttons .btn-primary:hover {
background-color: #1a91da;
}
.modal-buttons .btn-primary i {
font-size: 18px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@media (max-width: 768px) {
.modal-content {
padding: 30px 20px;
max-width: 90%;
}
.modal-content h2 {
font-size: 20px;
}
}

View File

@@ -39,13 +39,26 @@ namespace Submission.Api.Controllers
{
return Problem("Petitions Folder not found");
}
}
[HttpGet("petitions-list", Name = "GetPetitionsList")]
public IActionResult GetPetitionsList()
{
var list = _petitionRepository.FilterBy(x => x.Id != null);
return Ok(list);
}
[HttpGet("create-petition-folder", Name = "CreatePetitionFolder")]
public IActionResult create_petition_folder()
{
if (Directory.Exists("Petitions"))
{
return Ok("Petitions folder already exists");
}
try
{
Directory.CreateDirectory("Petitions");

View File

@@ -2,9 +2,11 @@ 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;
namespace Submission.Api.Controllers
{
@@ -17,62 +19,85 @@ namespace Submission.Api.Controllers
private readonly IMongoRepository<PetitionDetail> _detailRepository;
private readonly IMongoRepository<Signature> _signatureRepository;
private readonly IMemoryCache _cache;
public readonly TurnstileService _turnstileService;
public SignController(
IMongoRepository<Author> authorRepository,
IMongoRepository<PetitionDetail> detailRepository,
IMongoRepository<Signature> signatureRepository,
IMemoryCache cache)
IMemoryCache cache, TurnstileService turnstileService)
{
_authorRepository = authorRepository;
_detailRepository = detailRepository;
_signatureRepository = signatureRepository;
_cache = cache;
_turnstileService = turnstileService;
}
[HttpPost("petition/{petition_id}", Name = "SignPetition")]
[EnableRateLimiting("SignPetitionPolicy")]
public async Task<IActionResult> SignDisHoe([FromRoute] Guid petition_id, [FromBody] WidgetsDto body)
{
var cacheKey = $"petition_{petition_id}";
var remoteip = HttpContext.Request.Headers["CF-Connecting-IP"].FirstOrDefault() ??
HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault() ??
HttpContext.Connection.RemoteIpAddress?.ToString();
var pet = await _detailRepository.FindByIdAsync(petition_id);
if (body.turnstileToken == null)
return BadRequest("Turnstile token is missing");
if (pet == null)
return NotFound();
Console.WriteLine("Token received: " + body.turnstileToken);
//TODO : add svg validation
var validation = await _turnstileService.ValidateTokenAsync(body.turnstileToken, remoteip);
//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");
//add signature to the db
await _signatureRepository.InsertOneAsync(new Signature
if (validation.Success)
{
IdCard = body.IdCard,
Name = body.Name,
Signature_SVG = body.Signature,
Timestamp = DateTime.Now,
PetitionId = petition_id
});
//why??
var cacheKey = $"petition_{petition_id}";
//update signature count
if (pet.SignatureCount == null)
{
pet.SignatureCount = 0;
var pet = await _detailRepository.FindByIdAsync(petition_id);
if (pet == null)
return NotFound();
//TODO : add svg validation
//fuck i still havent done this
//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");
//add signature to the db
await _signatureRepository.InsertOneAsync(new Signature
{
IdCard = body.IdCard,
Name = body.Name,
Signature_SVG = body.Signature,
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");
}
else
{
// Invalid token - reject submission
return BadRequest($"Verification failed: {string.Join(", ", validation.ErrorCodes)}");
}
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("petition/{petition_id}", Name = "GetPetition")]
@@ -121,4 +146,63 @@ 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 const string SiteverifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
public TurnstileService(HttpClient httpClient, IOptions<TurnstileSettings> options)
{
_httpClient = httpClient;
_secretKey = options?.Value?.SecretKey ?? throw new ArgumentNullException(nameof(options), "Turnstile:SecretKey must be configured in appsettings.json");
}
public async Task<TurnstileResponse> ValidateTokenAsync(string token, string remoteip = null)
{
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();
return JsonSerializer.Deserialize<TurnstileResponse>(stringContent);
}
catch (Exception)
{
return new TurnstileResponse
{
Success = false,
ErrorCodes = new[] { "internal-error" }
};
}
}
}
public class TurnstileResponse
{
public bool Success { get; set; }
public string[] ErrorCodes { get; set; }
}
#endregion
}

View File

@@ -1,7 +1,7 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 9755
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

View File

@@ -7,12 +7,15 @@ public class WidgetsDto
[Required]
[MinLength(3)]
public string Name { get; set; }
[Required]
[MinLength(6)]
[MaxLength(7)]
public string IdCard { get; set; }
[Required]
public string Signature { get; set; }
[Required]
public string turnstileToken { get; set; }
}

View File

@@ -3,12 +3,14 @@ using Ashi.MongoInterface.Service;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Submission.Api.Configuration;
using Submission.Api.Controllers;
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.Configure<TurnstileSettings>(builder.Configuration.GetSection("Turnstile"));
builder.Services.AddSingleton<IMongoDbSettings>(serviceProvider =>
serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value);
@@ -22,6 +24,9 @@ builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register TurnstileService with typed HttpClient
builder.Services.AddHttpClient<TurnstileService>();
// Add rate limiting
builder.Services.AddRateLimiter(options =>
{
@@ -37,11 +42,11 @@ builder.Services.AddRateLimiter(options =>
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//if (app.Environment.IsDevelopment())
//{
app.UseSwagger();
app.UseSwaggerUI();
//}
app.UseHttpsRedirection();

View File

@@ -1,8 +1,19 @@
{
"Turnstile": {
"SecretKey": "your-turn"
},
"PetitionSettings": {
"AllowPetitionCreation": true
},
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "petition_database"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
},
"AllowedHosts": "*"
}

View File

@@ -4,4 +4,27 @@
build:
context: .
dockerfile: Submission.Api/Dockerfile
networks:
- app-network
expose:
- "8080"
environment:
- ASPNETCORE_URLS=http://+:8080
- MongoDbSettings__ConnectionString=mongodb://host.docker.internal:27017
nginx:
image: petition-nginx
build:
context: .
dockerfile: nginx/Dockerfile
ports:
- "9755:80"
depends_on:
- submission.api
networks:
- app-network
networks:
app-network:
driver: bridge

12
nginx/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM nginx:alpine
# Copy custom nginx configuration
COPY nginx/nginx.conf /etc/nginx/nginx.conf
# Copy static frontend files
COPY Frontend/ /usr/share/nginx/html/
# Expose port 80
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

53
nginx/nginx.conf Normal file
View File

@@ -0,0 +1,53 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;
server {
listen 80;
server_name _;
# Serve static frontend files
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Proxy API requests to the backend
location /api/ {
proxy_pass http://submission.api:8080/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Optional: Proxy Swagger if you want it accessible
location /swagger {
proxy_pass http://submission.api:8080/swagger;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View File

@@ -1,7 +1,7 @@
---
startDate: 14-12-2025
nameDhiv: "މިސާލު ނަން"
nameEng: "Sample Petition Name"
nameEng: "Petition for the Enactment of a Comprehensive Data Protection Act"
author:
name: "Fishie"
nid: "AAAAA12345"
@@ -13,5 +13,50 @@ author:
## 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.
We, the citizens of the Republic of Maldives, respectfully submit this petition to address the critical lack of a modern, comprehensive framework to protect the fundamental rights, privacy, and digital security of the Maldivian people.
In the rapidly evolving digital age, personal data including names, contact details, location data, and communication records is constantly being collected, processed, and stored by both private companies and public entities. The current legal framework in the Maldives is inadequate to safeguard these assets, leaving citizens vulnerable to privacy breaches, misuse of data, and exploitation. The absence of robust regulation has already led to severe real-world consequences, including high-profile incidents involving the unauthorized access, sale, and mass leakage of citizens personal and confidential information.
The widespread practice of sending unsolicited promotional SMS and messages is a clear example of this data processing abuse, constituting a daily intrusion into the personal lives and privacy of citizens, leading to significant disruption and eroding the quality of mobile communication.
---
A. Enactment of a Comprehensive Data Protection Act (Inspired by Global Standards)
We respectfully request the People's Majlis to initiate, deliberate upon, and enact a dedicated Data Protection Act that establishes high standards for the lawful processing of personal data, drawing upon globally recognized best practices, such as the European Unions General Data Protection Regulation (GDPR), to ensure the Maldives framework is robust and future-proof.
1 . The Act must include, but not be limited to, the following core principles:
- Fundamental Rights of the Data Subject: Guaranteeing the rights of individuals to:
- Access their personal data.
- Rectify inaccurate data.
- Erase their data (Right to be Forgotten).
- Portability of their data.
2 . Lawful Basis for Processing: Mandating that all processing of personal data must be based on a clear, explicit, and informed consent or other defined legal grounds. The use of pre-checked boxes or presumed consent must be prohibited.
3 . Specific Mandate on Electronic Marketing Consent (Unsolicited SMS):
- The Act must specifically define the use of personal data (such as a phone number) for promotional SMS or electronic messaging as a form of marketing that requires prior, specific, and informed Opt-In consent from the subscriber.
- This consent must be separate from any general terms and conditions, ensuring the default position for all promotional communication is Opt-In (where the sender must receive explicit consent) rather than Opt-Out.
- The Act must prohibit the sending of commercial, marketing, or promotional SMS and messages to any subscriber without this specific consent.
4 . Establishment of an Independent Authority: Creating a well-resourced and independent Data Protection Authority (DPA) with the power to:
- Enforce the Act.
- Investigate complaints.
- Impose significant and effective penalties for non-compliance.
4 . Data Breach Obligations: Making it mandatory for data controllers to promptly notify the DPA and affected individuals of any significant data breach.
---
We affirm that this petition is in full compliance with the Rules of Procedure of the People's Majlis (Article 257) and does not contain any of the prohibited content, including, but not limited to: anything contrary to the Constitution or laws of the Republic of Maldives; confidential business or financial information; any request to grant or strip honors, or give or dismiss employment; or anything that endangers national security.
---
We urge the Honourable People's Majlis to recognize the profound importance of digital privacy and the necessity of safeguarding citizens from intrusive communication. We respectfully pray that the Majlis accepts this petition, refers it to the appropriate Standing Committee for thorough review, and takes the necessary legislative steps to enact the requested Data Protection Act, incorporating the mandatory Opt-In policy for all promotional communication.
We look forward to the Majliss favourable consideration of this matter of national importance.