From 3e286f0c80312e21d94c7a2212b88998b3fd2cfa Mon Sep 17 00:00:00 2001 From: fISHIE <83373559+WhoIsFishie@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:10:09 +0500 Subject: [PATCH] idk --- README.md | 302 +++++++++++++++++++ Submission.Api.sln | 14 + Submission.Api/Controllers/SignController.cs | 103 +++++++ Submission.Api/Dto/Author.cs | 5 +- Submission.Api/Dto/PetitionDetailsDto.cs | 11 + Submission.Api/Dto/WidgetDto.cs | 16 +- Submission.Api/Models/Author.cs | 10 +- Submission.Api/Models/PetitionDetail.cs | 18 +- Submission.Api/Models/Widget.cs | 12 +- Submission.Api/Program.cs | 32 +- Submission.Api/Submission.Api.csproj | 12 + Submission.Api/Submission.Api.csproj.user | 6 + compose.yaml | 7 + 13 files changed, 534 insertions(+), 14 deletions(-) create mode 100644 README.md create mode 100644 Submission.Api/Controllers/SignController.cs create mode 100644 Submission.Api/Submission.Api.csproj.user create mode 100644 compose.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d52187 --- /dev/null +++ b/README.md @@ -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 +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": "..." +} +``` + +**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. \ No newline at end of file diff --git a/Submission.Api.sln b/Submission.Api.sln index b1d893a..6f8a699 100644 --- a/Submission.Api.sln +++ b/Submission.Api.sln @@ -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 diff --git a/Submission.Api/Controllers/SignController.cs b/Submission.Api/Controllers/SignController.cs new file mode 100644 index 0000000..f152f8c --- /dev/null +++ b/Submission.Api/Controllers/SignController.cs @@ -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 _authorRepository; + private readonly IMongoRepository _detailRepository; + private readonly IMongoRepository _signatureRepository; + private readonly IMemoryCache _cache; + + public SignController( + IMongoRepository authorRepository, + IMongoRepository detailRepository, + IMongoRepository signatureRepository, + IMemoryCache cache) + { + _authorRepository = authorRepository; + _detailRepository = detailRepository; + _signatureRepository = signatureRepository; + _cache = cache; + } + + [HttpPost(Name = "petition/{id}")] + [EnableRateLimiting("SignPetitionPolicy")] + public async Task 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 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); + } + } +} diff --git a/Submission.Api/Dto/Author.cs b/Submission.Api/Dto/Author.cs index ef72db9..a0f852f 100644 --- a/Submission.Api/Dto/Author.cs +++ b/Submission.Api/Dto/Author.cs @@ -1,6 +1,7 @@ namespace Submission.Api.Dto; -public class Author +public class AuthorsDto { - + public string Name { get; set; } + public string NID { get; set; } } \ No newline at end of file diff --git a/Submission.Api/Dto/PetitionDetailsDto.cs b/Submission.Api/Dto/PetitionDetailsDto.cs index 3e54b6f..bfe266c 100644 --- a/Submission.Api/Dto/PetitionDetailsDto.cs +++ b/Submission.Api/Dto/PetitionDetailsDto.cs @@ -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; } } \ No newline at end of file diff --git a/Submission.Api/Dto/WidgetDto.cs b/Submission.Api/Dto/WidgetDto.cs index 9fd6fc4..50cf297 100644 --- a/Submission.Api/Dto/WidgetDto.cs +++ b/Submission.Api/Dto/WidgetDto.cs @@ -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; } } \ No newline at end of file diff --git a/Submission.Api/Models/Author.cs b/Submission.Api/Models/Author.cs index c94cde3..feb03fb 100644 --- a/Submission.Api/Models/Author.cs +++ b/Submission.Api/Models/Author.cs @@ -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; } } \ No newline at end of file diff --git a/Submission.Api/Models/PetitionDetail.cs b/Submission.Api/Models/PetitionDetail.cs index 14bc007..31351ae 100644 --- a/Submission.Api/Models/PetitionDetail.cs +++ b/Submission.Api/Models/PetitionDetail.cs @@ -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; } } \ No newline at end of file diff --git a/Submission.Api/Models/Widget.cs b/Submission.Api/Models/Widget.cs index b026ac4..d7803bc 100644 --- a/Submission.Api/Models/Widget.cs +++ b/Submission.Api/Models/Widget.cs @@ -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; } } \ No newline at end of file diff --git a/Submission.Api/Program.cs b/Submission.Api/Program.cs index 666a9c5..baf5e08 100644 --- a/Submission.Api/Program.cs +++ b/Submission.Api/Program.cs @@ -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(builder.Configuration.GetSection("MongoDbSettings")); + +builder.Services.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService>().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(); \ No newline at end of file diff --git a/Submission.Api/Submission.Api.csproj b/Submission.Api/Submission.Api.csproj index 2d5df00..cb00eb4 100644 --- a/Submission.Api/Submission.Api.csproj +++ b/Submission.Api/Submission.Api.csproj @@ -4,10 +4,22 @@ net9.0 enable enable + Linux + + + + + + .dockerignore + + + + + diff --git a/Submission.Api/Submission.Api.csproj.user b/Submission.Api/Submission.Api.csproj.user new file mode 100644 index 0000000..9ff5820 --- /dev/null +++ b/Submission.Api/Submission.Api.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..a742e34 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,7 @@ +services: + submission.api: + image: submission.api + build: + context: . + dockerfile: Submission.Api/Dockerfile +