This commit is contained in:
fISHIE
2025-12-10 11:10:09 +05:00
parent 6c96716cfb
commit 3e286f0c80
13 changed files with 534 additions and 14 deletions

302
README.md Normal file
View File

@@ -0,0 +1,302 @@
# Petition Submission API
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.
## Features
- Sign petitions with digital signatures
- Retrieve petition details including author information
- Rate limiting (3 signatures per minute per IP)
- Duplicate signature prevention (one signature per ID card)
- MongoDB backend for data persistence
- Docker support for easy deployment
- Bilingual support (Dhivehi and English)
## Prerequisites
- .NET 9.0 SDK
- MongoDB instance
- Docker (optional, for containerized deployment)
## Configuration
### 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
### Application Configuration
Update `appsettings.json` with your MongoDB connection settings:
```json
{
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "petition_database"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
```
### Rate Limiting Configuration
The API is configured with rate limiting to prevent spam. Default settings in `Program.cs`:
- **Limit**: 3 signatures per minute per IP address
- **Window**: Fixed 1-minute window
- **Queue**: Disabled (requests over limit receive HTTP 429)
To modify rate limits, edit `Program.cs`:
```csharp
limiterOptions.PermitLimit = 3; // Change this number
limiterOptions.Window = TimeSpan.FromMinutes(1); // Change time window
```
## Installation
### Local Development
1. Clone the repository:
```bash
git clone <repository-url>
cd Submission.Api
```
2. Restore dependencies:
```bash
dotnet restore
```
3. Update `appsettings.json` with your MongoDB connection string
4. Run the application:
```bash
dotnet run --project Submission.Api
```
The API will be available at:
- HTTPS: `https://localhost:7xxx`
- HTTP: `http://localhost:5xxx`
### Docker Deployment
1. Build the Docker image:
```bash
docker build -t petition-api .
```
2. Run the container:
```bash
docker run -d -p 8080:8080 -p 8081:8081 \
-e MongoDbSettings__ConnectionString="mongodb://your-mongo-host:27017" \
-e MongoDbSettings__DatabaseName="petition_database" \
petition-api
```
## API Endpoints
### Sign a Petition
Signs a petition with user information and signature.
**Endpoint**: `POST /api/Sign`
**Rate Limit**: 3 requests per minute per IP
**Request Body**:
```json
{
"name": "John Doe",
"idCard": "A123456",
"signature": "<svg>...</svg>"
}
```
**Field Validation**:
- `name`: Required, minimum 3 characters
- `idCard`: Required, 6-7 characters (typically National ID)
- `signature`: Required, SVG signature data
**Success Response** (200 OK):
```json
{}
```
**Error Responses**:
- **400 Bad Request** - Invalid request body or validation failed
```json
{
"errors": {
"name": ["The field Name must be a string with a minimum length of 3."],
"idCard": ["The field IdCard must be a string with a minimum length of 6 and a maximum length of 7."]
}
}
```
- **429 Too Many Requests** - Rate limit exceeded
```json
{
"status": 429,
"title": "Too Many Requests"
}
```
- **500 Internal Server Error** - User already signed the petition
```json
{
"title": "You already signed this petition"
}
```
### Get Petition Details
Retrieves details of a specific petition including author information.
**Endpoint**: `GET /api/Sign/{petition_id}`
**URL Parameters**:
- `petition_id` (GUID) - The unique identifier of the petition
**Success Response** (200 OK):
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"startDate": "2025-01-15",
"nameDhiv": "ޕެޓިޝަން ނަން",
"nameEng": "Petition Name",
"authorDetails": {
"name": "Author Name",
"nid": "A123456"
},
"petitionBodyDhiv": "ޕެޓިޝަން ތަފްސީލް...",
"petitionBodyEng": "Petition description...",
"signatureCount": 0
}
```
**Error Response**:
- **404 Not Found** - Petition does not exist
```json
{}
```
## 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
### Rate Limiting
- Prevents spam by limiting signature submissions to 3 per minute per IP
- Uses ASP.NET Core built-in rate limiting middleware
- Returns HTTP 429 when limit is exceeded
### Duplicate Prevention
- Each ID card can only sign a petition once
- Checked before inserting into database
- Returns error message if duplicate detected
### Input Validation
- Name: Minimum 3 characters
- ID Card: Must be 6-7 characters (validates National ID format)
- Signature: Required field
## Development
### Project Structure
```
Submission.Api/
├── Controllers/
│ └── SignController.cs # API endpoints
├── Dto/
│ ├── WidgetsDto.cs # Signature request DTO
│ ├── PetitionDetailsDto.cs # Petition response DTO
│ └── Author.cs # Author DTO
├── Models/
│ ├── Widget.cs # Signature database model
│ ├── PetitionDetail.cs # Petition database model
│ └── Author.cs # Author database model
├── Program.cs # Application configuration
├── Dockerfile # Docker configuration
└── appsettings.json # Application settings
```
### Dependencies
- ASP.NET Core 9.0
- MongoDB Driver (via Ashi.MongoInterface)
- Microsoft.AspNetCore.OpenApi 9.0.11
- Microsoft.AspNetCore.RateLimiting (built-in)
## Troubleshooting
### Common Issues
**MongoDB Connection Failed**
- Verify MongoDB is running
- Check connection string in `appsettings.json`
- Ensure network connectivity to MongoDB instance
## Contributing
When contributing to this project:
1. Follow existing code style and conventions
2. Test all endpoints thoroughly
3. Update documentation for any API changes
4. Ensure rate limiting is not disabled in production
## License
if you use this you must mention that its powered by Mv Devs Union
also any forks must be open source
this must never be used for data collection and profiling people
## Support
For issues or questions, please open an issue on the repository.

View File

@@ -2,6 +2,13 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Submission.Api", "Submission.Api\Submission.Api.csproj", "{F061EFE3-FFFE-4F0F-825A-9FF8B985912D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{204A1F7E-0832-4DEF-A64E-EB1A00F559A0}"
ProjectSection(SolutionItems) = preProject
compose.yaml = compose.yaml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ashi.MongoInterface", "external\OtherRepo\Ashi.MongoInterface\Ashi.MongoInterface.csproj", "{6F6B8940-4740-48D1-8790-8CD82A66676B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -12,5 +19,12 @@ Global
{F061EFE3-FFFE-4F0F-825A-9FF8B985912D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F061EFE3-FFFE-4F0F-825A-9FF8B985912D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F061EFE3-FFFE-4F0F-825A-9FF8B985912D}.Release|Any CPU.Build.0 = Release|Any CPU
{6F6B8940-4740-48D1-8790-8CD82A66676B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F6B8940-4740-48D1-8790-8CD82A66676B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F6B8940-4740-48D1-8790-8CD82A66676B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F6B8940-4740-48D1-8790-8CD82A66676B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{6F6B8940-4740-48D1-8790-8CD82A66676B} = {204A1F7E-0832-4DEF-A64E-EB1A00F559A0}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,103 @@
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 Submission.Api.Dto;
using Submission.Api.Models;
namespace Submission.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class SignController : ControllerBase
{
private readonly IMongoRepository<Author> _authorRepository;
private readonly IMongoRepository<PetitionDetail> _detailRepository;
private readonly IMongoRepository<Widget> _signatureRepository;
private readonly IMemoryCache _cache;
public SignController(
IMongoRepository<Author> authorRepository,
IMongoRepository<PetitionDetail> detailRepository,
IMongoRepository<Widget> signatureRepository,
IMemoryCache cache)
{
_authorRepository = authorRepository;
_detailRepository = detailRepository;
_signatureRepository = signatureRepository;
_cache = cache;
}
[HttpPost(Name = "petition/{id}")]
[EnableRateLimiting("SignPetitionPolicy")]
public async Task<IActionResult> SignDisHoe([FromRoute]Guid petition_id,[FromBody] WidgetsDto body)
{
//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 Widget
{
IdCard = body.IdCard,
Name = body.Name,
Signature_SVG = body.Signature,
Timestamp = DateTime.Now
});
//update signature count
return Ok("your signature has been submitted");
}
[HttpGet(Name = "petition/{id}")]
public async Task<IActionResult> GetDisHoe([FromRoute] Guid petition_id)
{
var cacheKey = $"petition_{petition_id}";
// Try to get from cache
if (_cache.TryGetValue(cacheKey, out PetitionDetailsDto cachedDto))
{
return Ok(cachedDto);
}
// Not in cache, fetch from database
var pet = await _detailRepository.FindByIdAsync(petition_id);
if (pet == null)
return NotFound();
var author = await _authorRepository.FindOneAsync(x => x.Id == pet.AuthorId);
var dto = new PetitionDetailsDto
{
Id = petition_id,
NameDhiv = pet.NameDhiv,
StartDate = pet.StartDate,
NameEng = pet.NameEng,
PetitionBodyDhiv = pet.PetitionBodyDhiv,
PetitionBodyEng = pet.PetitionBodyEng,
AuthorDetails = new AuthorsDto
{
Name = author.Name,
NID = author.NID,
}
};
// Store in cache with 5 minute expiration
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(12));
_cache.Set(cacheKey, dto, cacheOptions);
return Ok(dto);
}
}
}

View File

@@ -1,6 +1,7 @@
namespace Submission.Api.Dto;
public class Author
public class AuthorsDto
{
public string Name { get; set; }
public string NID { get; set; }
}

View File

@@ -2,5 +2,16 @@
public class PetitionDetailsDto
{
public Guid Id { get; set; }
public DateOnly StartDate { get; set; }
public string NameDhiv { get; set; }
public string NameEng { get; set; }
public AuthorsDto AuthorDetails { get; set; }
public string PetitionBodyDhiv { get; set; }
public string PetitionBodyEng { get; set; }
public int SignatureCount { get; set; }
}

View File

@@ -1,6 +1,18 @@
namespace Submission.Api.Dto;
using System.ComponentModel.DataAnnotations;
public class WidgetDto
namespace Submission.Api.Dto;
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; }
}

View File

@@ -1,6 +1,10 @@
namespace Submission.Api.Models;
using Ashi.MongoInterface.Helper;
public class Author
namespace Submission.Api.Models;
[BsonCollection("author")]
public class Author : Document
{
public string Name { get; set; }
public string NID { get; set; }
}

View File

@@ -1,6 +1,20 @@
namespace Submission.Api.Models;
using System.Runtime.InteropServices;
using Ashi.MongoInterface.Helper;
public class PetitionDetail
namespace Submission.Api.Models;
[BsonCollection("petitionDetail")]
public class PetitionDetail : Document
{
public DateOnly StartDate { get; set; }
public string NameDhiv { get; set; }
public string NameEng { get; set; }
public Guid AuthorId { get; set; }
public string PetitionBodyDhiv { get; set; }
public string PetitionBodyEng { get; set; }
public int SignatureCount { get; set; }
}

View File

@@ -1,6 +1,12 @@
namespace Submission.Api.Models;
using Ashi.MongoInterface.Helper;
public class Widget
namespace Submission.Api.Models;
[BsonCollection("signatures")]
public class Widget : Document
{
public string Name { get; set; }
public string IdCard { get; set; }
public string Signature_SVG { get; set; }
public DateTime Timestamp { get; set; }
}

View File

@@ -1,11 +1,37 @@
var builder = WebApplication.CreateBuilder(args);
using System.Configuration;
using Ashi.MongoInterface;
using Ashi.MongoInterface.Service;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.Configure<MongoDbSettings>(builder.Configuration.GetSection("MongoDbSettings"));
builder.Services.AddSingleton<IMongoDbSettings>(serviceProvider =>
serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value);
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 rate limiting
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("SignPetitionPolicy", limiterOptions =>
{
limiterOptions.PermitLimit = 3;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueProcessingOrder = System.Threading.RateLimiting.QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 0;
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -16,8 +42,10 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection();
app.UseRateLimiter();
app.UseAuthorization();
app.MapControllers();
app.Run();
app.Run();

View File

@@ -4,10 +4,22 @@
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11"/>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.11" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\external\OtherRepo\Ashi.MongoInterface\Ashi.MongoInterface.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
</PropertyGroup>
</Project>

7
compose.yaml Normal file
View File

@@ -0,0 +1,7 @@
services:
submission.api:
image: submission.api
build:
context: .
dockerfile: Submission.Api/Dockerfile