diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c2c1784 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(docker compose build:*)", + "Bash(docker compose:*)" + ] + } +} diff --git a/DOCKER_README.md b/DOCKER_README.md new file mode 100644 index 0000000..b51021d --- /dev/null +++ b/DOCKER_README.md @@ -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 diff --git a/Frontend/index.html b/Frontend/index.html index eb1cfcd..21e02ac 100644 --- a/Frontend/index.html +++ b/Frontend/index.html @@ -7,6 +7,12 @@ + + +
@@ -69,7 +75,13 @@
-
+
+ + +
+ + + diff --git a/Frontend/style.css b/Frontend/style.css index ba5395a..e400742 100644 --- a/Frontend/style.css +++ b/Frontend/style.css @@ -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; + } +} diff --git a/Submission.Api/Controllers/DebugController.cs b/Submission.Api/Controllers/DebugController.cs index fa2389c..db68f39 100644 --- a/Submission.Api/Controllers/DebugController.cs +++ b/Submission.Api/Controllers/DebugController.cs @@ -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"); diff --git a/Submission.Api/Controllers/SignController.cs b/Submission.Api/Controllers/SignController.cs index 3ce765e..7ed7dbd 100644 --- a/Submission.Api/Controllers/SignController.cs +++ b/Submission.Api/Controllers/SignController.cs @@ -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 _detailRepository; private readonly IMongoRepository _signatureRepository; private readonly IMemoryCache _cache; + public readonly TurnstileService _turnstileService; public SignController( IMongoRepository authorRepository, IMongoRepository detailRepository, IMongoRepository 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 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.Filter.Eq("_id", petition_id); + var Countupdate = Builders.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.Filter.Eq("_id", petition_id); - var Countupdate = Builders.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 options) + { + _httpClient = httpClient; + _secretKey = options?.Value?.SecretKey ?? throw new ArgumentNullException(nameof(options), "Turnstile:SecretKey must be configured in appsettings.json"); + } + + public async Task ValidateTokenAsync(string token, string remoteip = null) + { + var parameters = new Dictionary + { + { "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(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 } diff --git a/Submission.Api/Dockerfile b/Submission.Api/Dockerfile index 5960e3c..bfe370f 100644 --- a/Submission.Api/Dockerfile +++ b/Submission.Api/Dockerfile @@ -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 diff --git a/Submission.Api/Dto/WidgetDto.cs b/Submission.Api/Dto/WidgetDto.cs index 50cf297..c0631bb 100644 --- a/Submission.Api/Dto/WidgetDto.cs +++ b/Submission.Api/Dto/WidgetDto.cs @@ -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; } } \ No newline at end of file diff --git a/Submission.Api/Program.cs b/Submission.Api/Program.cs index 6bf1f98..baf4684 100644 --- a/Submission.Api/Program.cs +++ b/Submission.Api/Program.cs @@ -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(builder.Configuration.GetSection("MongoDbSettings")); builder.Services.Configure(builder.Configuration.GetSection("PetitionSettings")); +builder.Services.Configure(builder.Configuration.GetSection("Turnstile")); builder.Services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService>().Value); @@ -22,6 +24,9 @@ builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// Register TurnstileService with typed HttpClient +builder.Services.AddHttpClient(); + // 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(); diff --git a/Submission.Api/appsettings.Development.json b/Submission.Api/appsettings.Development.json index 0c208ae..967a15b 100644 --- a/Submission.Api/appsettings.Development.json +++ b/Submission.Api/appsettings.Development.json @@ -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": "*" } diff --git a/compose.yaml b/compose.yaml index a742e34..e142b87 100644 --- a/compose.yaml +++ b/compose.yaml @@ -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 diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..416a1cd --- /dev/null +++ b/nginx/Dockerfile @@ -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;"] diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..83511c6 --- /dev/null +++ b/nginx/nginx.conf @@ -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; + } + } +} diff --git a/sample.Petition.md b/sample.Petition.md index 82b96f1..307b1e4 100644 --- a/sample.Petition.md +++ b/sample.Petition.md @@ -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. \ No newline at end of file +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 Union’s 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 Majlis’s favourable consideration of this matter of national importance. \ No newline at end of file