Skip to content

Commit 7f42e0e

Browse files
Merge pull request #35 from ScriptSage001/master
Added restriction for the number of short links per month and added Global Rate Limiting
2 parents e2aeb45 + 237ada2 commit 7f42e0e

File tree

17 files changed

+351
-22
lines changed

17 files changed

+351
-22
lines changed

Assets/Images/Logo.png

40.1 KB
Loading

Assets/Images/Swagger UI 0.png

104 KB
Loading

Assets/Images/Swagger UI 1.png

94.9 KB
Loading

Assets/Images/Swagger UI 2.png

102 KB
Loading

README.md

Lines changed: 155 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,156 @@
1-
<h1 align="center" id="title">Shortify.NET (In-Progress)</h1>
1+
<h1 align="center" id="title">Shortify.NET</h1>
2+
3+
<p align="center"><img src="https://github.com/ScriptSage001/Shortify.NET/blob/master/Assets/Images/Logo.png?raw=true" alt="logo"></p>
24

35
<p align="center"><img src="https://socialify.git.ci/ScriptSage001/Shortify.NET/image?description=1&amp;descriptionEditable=A%20powerful%20.NET%208%20URL%20shortener%20with%20JWT%20auth%2C%20analytics%2C%20and%20caching%2C%20designed%20for%20scalability%20and%20security.&amp;font=Raleway&amp;language=1&amp;name=1&amp;owner=1&amp;pattern=Plus&amp;theme=Dark" alt="project-image"></p>
46

57
<p id="description">A powerful .NET 8 URL shortener with JWT auth analytics and caching designed for scalability and security.</p>
68

9+
<br>
10+
711
<h3>Code Quality</h3>
812

913
[![Qodana](https://github.com/ScriptSage001/Shortify.NET/actions/workflows/qodana_code_quality.yml/badge.svg)](https://github.com/ScriptSage001/Shortify.NET/actions/workflows/qodana_code_quality.yml)
1014

11-
<h2>🚀 Swagger UI</h2>
15+
<br>
16+
17+
<h2>✨ Why Use Shortify.NET?</h2>
18+
19+
- **Scalable and Secure:** Built with .NET 8, ensuring high performance and strong security measures.
20+
- **JWT Authentication:** Secure user management and role-based access control.
21+
- **Caching:** Accelerated redirection using Redis for optimal performance.
22+
- **Ease of Use:** A simple API interface, designed to integrate seamlessly into any application.
23+
- **Analytics:** Gain insights into URL usage patterns. (Coming Soon)
24+
25+
<br>
26+
27+
<h2>🧩 Features</h2>
1228

13-
[https://shortify-net.onrender.com/swagger/index.html](https://shortify-net.onrender.com/swagger/index.html)
29+
- Generate short URLs quickly and efficiently.
30+
- Secure endpoints with JWT authentication.
31+
- Track detailed analytics for shortened URLs.
32+
- Support for custom aliases.
33+
- Scalable design for high availability.
34+
- Dockerized for easy deployment.
1435

15-
<h2>🛠️ Installation Steps:</h2>
36+
<br>
1637

17-
<p>1. docker pull</p>
38+
<h2>🚀 Tech Stack</h2>
39+
40+
- **Backend:** .NET 8
41+
- **Database:** PostgreSQL (Supabase-hosted)
42+
- **Caching:** Redis
43+
- **Containerization:** Docker
44+
- **API Documentation:** Swagger
45+
- **CI/CD:** GitHub Actions
46+
- **Hosting Platform:** Render
47+
- **Static Code Analysis:** Qodana
48+
49+
<br>
50+
51+
<h2>🛠️ Installation and Setup:</h2>
52+
<h3>Using Docker</h3>
53+
54+
<p>1. Pull the Docker Image:</p>
1855

1956
```
2057
docker pull thescriptsage/shortifynetapi
2158
```
59+
<br>
60+
61+
<p>2. Set Up Required Environment Variables:</p>
62+
Ensure the following environment variables are set before running the container:
63+
64+
- **DB_CONNECTION:** Connection string for the PostgreSQL database (e.g., Host=localhost;Port=5432;Database=Shortify;Username=yourUsername;Password=yourPassword).
65+
- **REDIS_CONNECTION:** Connection string for Redis (e.g., localhost:6379).
66+
- **APP_SECRET:** A secret key for signing JWT tokens.
67+
- **CLIENT_SECRET:** Client-specific secret for enhanced security.
68+
- **SENDER_EMAIL:** Email address for sending OTPs or notifications.
69+
- **SENDER_EMAIL_PASSWORD:** Password for the sender email.
70+
- **ALLOWED_HOST:** A comma-separated list of allowed host URLs.
71+
72+
<br>
73+
74+
<p>3. Run the Docker Container:</p>
75+
76+
```
77+
docker run -d -p 5000:80 \
78+
-e DB_CONNECTION="Host=localhost;Port=5432;Database=Shortify;Username=yourUsername;Password=yourPassword" \
79+
-e REDIS_CONNECTION="localhost:6379" \
80+
-e APP_SECRET="yourAppSecret" \
81+
-e CLIENT_SECRET="yourClientSecret" \
82+
-e SENDER_EMAIL="yourEmail@gmail.com" \
83+
-e SENDER_EMAIL_PASSWORD="yourEmailPassword" \
84+
-e ALLOWED_HOST="http://localhost,http://example.com" \
85+
thescriptsage/shortifynetapi
86+
```
87+
<br>
2288

23-
<p>2. docker run</p>
89+
<p>3. Access the Swagger UI:</p>
90+
Visit <a target="_blank" href="http://localhost:5000/swagger/index.html">http://localhost:5000/swagger/index.html</a> to explore the API.
91+
92+
<br><br>
93+
<h3>Local Development</h3>
94+
95+
<p>1. Clone the Repository:</p>
2496

2597
```
26-
docker run -d -p 5000:80 thescriptsage/shortifynetapi
98+
git clone https://github.com/ScriptSage001/Shortify.NET.git
99+
cd Shortify.NET
27100
```
101+
<br>
102+
103+
<p>2. Configure Environment Variables:</p>
104+
Use an environment variable manager or .env file to configure the following values:
105+
106+
- **DB_CONNECTION:** Connection string for the PostgreSQL database (e.g., Host=localhost;Port=5432;Database=Shortify;Username=yourUsername;Password=yourPassword).
107+
- **REDIS_CONNECTION:** Connection string for Redis (e.g., localhost:6379).
108+
- **APP_SECRET:** A secret key for signing JWT tokens.
109+
- **CLIENT_SECRET:** Client-specific secret for enhanced security.
110+
- **SENDER_EMAIL:** Email address for sending OTPs or notifications.
111+
- **SENDER_EMAIL_PASSWORD:** Password for the sender email.
112+
- **ALLOWED_HOST:** A comma-separated list of allowed host URLs.
113+
<br>
114+
<h4>Example .env file:</h4>
115+
116+
```
117+
DB_CONNECTION=Host=localhost;Port=5432;Database=Shortify;Username=yourUsername;Password=yourPassword
118+
REDIS_CONNECTION=localhost:6379
119+
APP_SECRET=yourAppSecret
120+
CLIENT_SECRET=yourClientSecret
121+
SENDER_EMAIL=yourEmail@gmail.com
122+
SENDER_EMAIL_PASSWORD=yourEmailPassword
123+
ALLOWED_HOST=http://localhost,http://example.com
124+
```
125+
<br>
126+
127+
<p>3. Install Dependencies:</p>
128+
Ensure you have the .NET 8 SDK installed. Then, restore the NuGet packages:
129+
130+
```
131+
dotnet restore
132+
```
133+
<br>
134+
135+
<p>4. Run the Application:</p>
136+
137+
```
138+
dotnet run
139+
```
140+
<br>
141+
142+
<p>5. Access the Swagger UI:</p>
143+
<p>Swagger UI will be available at <a target="_blank" href="http://localhost:5000/swagger/index.html">http://localhost:5000/swagger/index.html</a> or the port specified in the console logs.</p>
144+
145+
<br>
146+
147+
<h2>📚 API Documentation</h2>
148+
The API is fully documented using Swagger. Access the live documentation here:
149+
150+
- <a target="_blank" href="https://shortify-net.onrender.com/swagger/index.html">Swagger UI</a>
151+
- <a target="_blank" href="https://shortify-net.onrender.com/swagger/v1/swagger.json">Swagger JSON</a>
152+
153+
<br>
28154

29155
<h2>🍰 Contribution Guidelines:</h2>
30156

@@ -38,21 +164,41 @@ docker run -d -p 5000:80 thescriptsage/shortifynetapi
38164
```
39165
git checkout -b feature/AmazingFeature
40166
```
167+
<br>
41168

42169
<p>3. Commit your Changes</p>
43170

44171
```
45-
git commit -m &#39;Add some AmazingFeature&#39;
172+
git commit -m 'Add some AmazingFeature'
46173
```
174+
<br>
47175

48176
<p>4. Push to the Branch</p>
49177

50178
```
51179
git push origin feature/AmazingFeature
52180
```
181+
<br>
53182

54183
<p>5. Open a Pull Request</p>
55184

185+
<br>
186+
187+
<h2>📸 Screenshots</h2>
188+
189+
<h3>Swagger UI</h3>
190+
<p align="center"><img src="https://github.com/ScriptSage001/Shortify.NET/blob/master/Assets/Images/Swagger%20UI%200.png?raw=true" alt="swagger-ui-01"></p>
191+
<p align="center"><img src="https://github.com/ScriptSage001/Shortify.NET/blob/master/Assets/Images/Swagger%20UI%201.png?raw=true" alt="swagger-ui-02"></p>
192+
<p align="center"><img src="https://github.com/ScriptSage001/Shortify.NET/blob/master/Assets/Images/Swagger%20UI%202.png?raw=true" alt="swagger-ui-03"></p>
193+
194+
<br>
195+
56196
<h2>🛡️ License:</h2>
197+
<p>This project is licensed under the Apache License. See the <a href="https://github.com/ScriptSage001/Shortify.NET/blob/master/LICENSE.txt">LICENSE</a> file for details.</p>
198+
199+
<br>
200+
201+
<h2>🌟 Acknowledgments</h2>
57202

58-
This project is licensed under the Apache License
203+
- Inspiration from modern URL shorteners like Bitly.
204+
- Thanks to the .NET community for continuous support and tools.

Shortify.NET.API/BaseApiController.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.AspNetCore.Mvc;
1+
using System.Security.Claims;
2+
using Microsoft.AspNetCore.Mvc;
23
using Microsoft.AspNetCore.Mvc.ModelBinding;
34
using Shortify.NET.Common.FunctionalTypes;
45
using Shortify.NET.Common.Messaging.Abstractions;
@@ -113,5 +114,20 @@ protected string GetUser()
113114

114115
return userIdClaims is null ? string.Empty : userIdClaims.Value;
115116
}
117+
118+
/// <summary>
119+
/// Checks if the user is an Admin User
120+
/// </summary>
121+
/// <returns></returns>
122+
protected bool IsUserAdmin()
123+
{
124+
var isUserAdmin = User
125+
.Claims
126+
.Any(c =>
127+
c.Type.Equals(ClaimTypes.Role, StringComparison.OrdinalIgnoreCase) &&
128+
c.Value == "Admin");
129+
130+
return isUserAdmin;
131+
}
116132
}
117133
}

Shortify.NET.API/Controllers/V1/ShortController.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Shortify.NET.API.Contracts;
55
using Shortify.NET.API.Mappers;
66
using Shortify.NET.Application.Url.Commands.DeleteUrl;
7+
using Shortify.NET.Application.Url.Queries.CanCreateShortUrl;
78
using Shortify.NET.Application.Url.Queries.GetAllShortenedUrls;
89
using Shortify.NET.Application.Url.Queries.GetOriginalUrl;
910
using Shortify.NET.Application.Url.Queries.ShortenedUrl;
@@ -67,13 +68,31 @@ public async Task<IActionResult> ShortenUrl(
6768
return HandleUnauthorizedRequest();
6869
}
6970

71+
var canCreate = true;
72+
73+
if(!IsUserAdmin())
74+
{
75+
canCreate = (await _apiService
76+
.RequestAsync(new CanCreateShortLinkQuery(userId), cancellationToken))
77+
.Value;
78+
}
79+
80+
if (!canCreate)
81+
{
82+
return HandleFailure(
83+
Result.Failure(
84+
Error.BadRequest(
85+
"Error.LimitReached",
86+
"Monthly limit of 10 short links reached.")));
87+
}
88+
7089
var command = _mapper.ShortenUrlRequestToCommand(request, userId, HttpContext.Request);
7190

7291
var response = await _apiService.SendAsync(command, cancellationToken);
7392

7493
return response.IsFailure ?
75-
HandleFailure(response) :
76-
Created(nameof(ShortenUrl), response.Value.Value);
94+
HandleFailure(response) :
95+
Created(nameof(ShortenUrl), response.Value.Value);
7796
}
7897

7998
/// <summary>

Shortify.NET.API/DependencyInjection.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
using System.Reflection;
1+
using System.Net;
2+
using System.Reflection;
23
using System.Text;
4+
using System.Threading.RateLimiting;
35
using Asp.Versioning;
46
using Microsoft.AspNetCore.Authentication.JwtBearer;
57
using Microsoft.Extensions.Options;
68
using Microsoft.IdentityModel.Tokens;
79
using Microsoft.OpenApi.Models;
10+
using Shortify.NET.API.Helpers;
811
using Shortify.NET.API.SwaggerConfig;
912
using Swashbuckle.AspNetCore.SwaggerGen;
1013

@@ -20,6 +23,7 @@ public static IServiceCollection AddApi(this IServiceCollection services, IConfi
2023
services.AddEndpointsApiExplorer();
2124
services.AddSwagger();
2225
services.AddCorsPolicy(configuration);
26+
services.AddRateLimiting(configuration);
2327

2428
return services;
2529
}
@@ -124,5 +128,46 @@ private static void AddCorsPolicy(this IServiceCollection services, IConfigurati
124128
});
125129
});
126130
}
131+
132+
private static void AddRateLimiting(this IServiceCollection services, IConfiguration configuration)
133+
{
134+
services.AddRateLimiter(options =>
135+
{
136+
options.OnRejected = (context, cancellationToken) =>
137+
{
138+
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
139+
context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", cancellationToken);
140+
return new ValueTask();
141+
};
142+
143+
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
144+
{
145+
var remoteIpAddress = context.Connection.RemoteIpAddress;
146+
147+
if (IPAddress.IsLoopback(remoteIpAddress!))
148+
return RateLimitPartition.GetNoLimiter(IPAddress.Loopback.ToString());
149+
150+
var rateLimiterOptions = configuration
151+
.GetSection("RateLimiterOptions")
152+
.Get<RateLimiterOptions>();
153+
154+
if (rateLimiterOptions is not null)
155+
{
156+
return RateLimitPartition.GetSlidingWindowLimiter(
157+
remoteIpAddress?.ToString()!,
158+
_ =>
159+
new SlidingWindowRateLimiterOptions
160+
{
161+
PermitLimit = rateLimiterOptions.PermitLimit,
162+
Window = TimeSpan.FromSeconds(rateLimiterOptions.WindowInSeconds),
163+
SegmentsPerWindow = rateLimiterOptions.SegmentsPerWindow,
164+
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
165+
QueueLimit = rateLimiterOptions.QueueLimit
166+
});
167+
}
168+
return RateLimitPartition.GetNoLimiter(IPAddress.Loopback.ToString());
169+
});
170+
});
171+
}
127172
}
128173
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace Shortify.NET.API.Helpers
2+
{
3+
/// <summary>
4+
/// Defines the options to configure the rate limiter
5+
/// </summary>
6+
public class RateLimiterOptions
7+
{
8+
/// <summary>
9+
/// Gets or Sets the number of request permitted per window
10+
/// </summary>
11+
public int PermitLimit { get; init; }
12+
13+
/// <summary>
14+
/// Gets or Sets the timespan of one window in seconds
15+
/// </summary>
16+
public int WindowInSeconds { get; init; }
17+
18+
/// <summary>
19+
/// Gets or Sets the number of segments the window is divided into
20+
/// </summary>
21+
public int SegmentsPerWindow { get; init; }
22+
23+
/// <summary>
24+
/// Gets or Sets the number of requests permitted in the queue.
25+
/// Pass 0 for no queue.
26+
/// </summary>
27+
public int QueueLimit { get; init; }
28+
}
29+
}

Shortify.NET.API/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959

6060
app.UseHttpsRedirection();
6161

62+
app.UseRateLimiter();
63+
6264
app.UseAuthentication();
6365
app.UseAuthorization();
6466

0 commit comments

Comments
 (0)