mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-10-25 11:17:28 +02:00 
			
		
		
		
	Login
This commit is contained in:
		
							parent
							
								
									cadc8e499d
								
							
						
					
					
						commit
						35a998b934
					
				| @ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.beans.factory.annotation.Qualifier; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.security.core.GrantedAuthority; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.security.core.userdetails.UserDetailsService; | ||||
| import org.springframework.stereotype.Component; | ||||
| @ -23,10 +24,12 @@ import jakarta.servlet.FilterChain; | ||||
| import jakarta.servlet.ServletException; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import stirling.software.SPDF.model.Role; | ||||
| @Component | ||||
| public class UserBasedRateLimitingFilter extends OncePerRequestFilter { | ||||
| 
 | ||||
|     private final Map<String, Bucket> buckets = new ConcurrentHashMap<>(); | ||||
| 	private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>(); | ||||
| 
 | ||||
|     @Autowired | ||||
|     private UserDetailsService userDetailsService; | ||||
| @ -39,7 +42,6 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { | ||||
|     protected void doFilterInternal(HttpServletRequest request, | ||||
|                                     HttpServletResponse response, | ||||
|                                     FilterChain filterChain) throws ServletException, IOException { | ||||
| 
 | ||||
|         if (!rateLimit) { | ||||
|             // If rateLimit is not enabled, just pass all requests without rate limiting | ||||
|             filterChain.doFilter(request, response); | ||||
| @ -47,7 +49,6 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { | ||||
|         } | ||||
| 
 | ||||
|         String method = request.getMethod(); | ||||
| 
 | ||||
|         if (!"POST".equalsIgnoreCase(method)) { | ||||
|             // If the request is not a POST, just pass it through without rate limiting | ||||
|             filterChain.doFilter(request, response); | ||||
| @ -73,7 +74,34 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { | ||||
|             identifier = request.getRemoteAddr(); | ||||
|         } | ||||
| 
 | ||||
|         Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket()); | ||||
|         Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); | ||||
| 
 | ||||
|         if (request.getHeader("X-API-Key") != null) { | ||||
|             // It's an API call | ||||
|             processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain); | ||||
|         } else { | ||||
|             // It's a Web UI call | ||||
|             processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private Role getRoleFromAuthentication(Authentication authentication) { | ||||
|         if (authentication != null && authentication.isAuthenticated()) { | ||||
|             for (GrantedAuthority authority : authentication.getAuthorities()) { | ||||
|                 try { | ||||
|                     return Role.fromString(authority.getAuthority()); | ||||
|                 } catch (IllegalArgumentException ex) { | ||||
|                     // Ignore and continue to next authority. | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         throw new IllegalStateException("User does not have a valid role."); | ||||
|     } | ||||
| 
 | ||||
|     private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets, | ||||
|                                 HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||||
|             throws IOException, ServletException { | ||||
|         Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); | ||||
|         ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); | ||||
| 
 | ||||
|         if (probe.isConsumed()) { | ||||
| @ -84,12 +112,11 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { | ||||
|             response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); | ||||
|             response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill)); | ||||
|             response.getWriter().write("Rate limit exceeded for POST requests."); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private Bucket createUserBucket() { | ||||
|         Bandwidth limit = Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofDays(1))); | ||||
|     private Bucket createUserBucket(int limitPerDay) { | ||||
|         Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); | ||||
|         return Bucket.builder().addLimit(limit).build(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -19,9 +19,10 @@ public class InitialSetup { | ||||
|             String initialPassword = System.getenv("INITIAL_PASSWORD"); | ||||
|             if(initialUsername != null && initialPassword != null) { | ||||
|                 userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); | ||||
|             } else { | ||||
|             	userService.saveUser("admin", "password", Role.ADMIN.getRoleId()); | ||||
|             } | ||||
| //             else { | ||||
| //            	userService.saveUser("admin", "password", Role.ADMIN.getRoleId()); | ||||
| //            } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -38,6 +38,13 @@ public enum Role { | ||||
|         return webCallsPerDay; | ||||
|     } | ||||
| 	     | ||||
| 	     | ||||
|     public static Role fromString(String roleId) { | ||||
|         for (Role role : Role.values()) { | ||||
|             if (role.getRoleId().equalsIgnoreCase(roleId)) { | ||||
|                 return role; | ||||
|             } | ||||
|         } | ||||
|         throw new IllegalArgumentException("No Role defined for id: " + roleId); | ||||
|     } | ||||
| 	 | ||||
| } | ||||
|  | ||||
							
								
								
									
										4
									
								
								src/main/resources/static/images/clipboard.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/main/resources/static/images/clipboard.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16"> | ||||
|   <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/> | ||||
|   <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 496 B | 
| @ -68,21 +68,34 @@ | ||||
| 					            <div class="input-group mb-3"> | ||||
| 					                <input type="password" class="form-control" id="apiKey" placeholder="Your API Key" readonly> | ||||
| 					                <div class="input-group-append"> | ||||
| 					                	<button class="btn btn-outline-secondary" id="copyBtn" type="button" onclick="copyToClipboard()">  | ||||
| 					                    	<img src="images/clipboard.svg" alt="Copy" style="height:20px;"> | ||||
| 					                	</button> | ||||
| 					                    <button class="btn btn-outline-secondary" id="showBtn" type="button" onclick="showApiKey()">👁️ Show</button> | ||||
| 					                    <button class="btn btn-outline-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()">🔄 Refresh</button> | ||||
| 					                     | ||||
| 					                </div> | ||||
| 					            </div> | ||||
| 					        </div> | ||||
| 					    </div> | ||||
| 					     | ||||
| 					    <script> | ||||
| 					    function copyToClipboard() { | ||||
| 					        const apiKeyElement = document.getElementById("apiKey"); | ||||
| 					        apiKeyElement.select(); | ||||
| 					        document.execCommand("copy"); | ||||
| 					    } | ||||
| 
 | ||||
| 					     | ||||
| 					    function showApiKey() { | ||||
| 					        const apiKeyElement = document.getElementById("apiKey"); | ||||
| 					        const copyBtn = document.getElementById("copyBtn"); | ||||
| 					        if (apiKeyElement.type === "password") { | ||||
| 					            apiKeyElement.type = "text"; | ||||
| 					        	apiKeyElement.type = "text"; | ||||
| 					        	copyBtn.disabled = false;  // Enable copy button when API key is visible | ||||
| 					        } else { | ||||
| 					            apiKeyElement.type = "password"; | ||||
| 					            copyBtn.disabled = true;  // Disable copy button when API key is hidden | ||||
| 					        } | ||||
| 					    } | ||||
| 
 | ||||
| @ -107,6 +120,7 @@ | ||||
| 					                let apiKey = await response.text(); | ||||
| 					                manageUIState(apiKey); | ||||
| 					                document.getElementById("apiKey").type = 'text'; | ||||
| 					                document.getElementById("copyBtn").disabled = false; | ||||
| 					            } else { | ||||
| 					                alert('Error refreshing API key.'); | ||||
| 					            } | ||||
| @ -118,13 +132,16 @@ | ||||
| 					    function manageUIState(apiKey) { | ||||
| 					        const apiKeyElement = document.getElementById("apiKey"); | ||||
| 					        const showBtn = document.getElementById("showBtn"); | ||||
| 					        const copyBtn = document.getElementById("copyBtn"); | ||||
| 
 | ||||
| 					        if (apiKey && apiKey.trim().length > 0) { | ||||
| 					            apiKeyElement.value = apiKey; | ||||
| 					            showBtn.disabled = false; | ||||
| 					            copyBtn.disabled = true;   | ||||
| 					        } else { | ||||
| 					            apiKeyElement.value = ""; | ||||
| 					            showBtn.disabled = true; | ||||
| 					            copyBtn.disabled = true; | ||||
| 					        } | ||||
| 					    } | ||||
| 
 | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| <!--   Hi if you have been redirected here when using API then you might need to supply a X-API-KEY key in header to authenticate! --> | ||||
| <!DOCTYPE html> | ||||
| <html xmlns:th="http://www.thymeleaf.org"> | ||||
| <head> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user