mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-11-01 01:21:18 +01: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.beans.factory.annotation.Qualifier;
 | 
				
			||||||
import org.springframework.http.HttpStatus;
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
import org.springframework.security.core.Authentication;
 | 
					import org.springframework.security.core.Authentication;
 | 
				
			||||||
 | 
					import org.springframework.security.core.GrantedAuthority;
 | 
				
			||||||
import org.springframework.security.core.context.SecurityContextHolder;
 | 
					import org.springframework.security.core.context.SecurityContextHolder;
 | 
				
			||||||
import org.springframework.security.core.userdetails.UserDetailsService;
 | 
					import org.springframework.security.core.userdetails.UserDetailsService;
 | 
				
			||||||
import org.springframework.stereotype.Component;
 | 
					import org.springframework.stereotype.Component;
 | 
				
			||||||
@ -23,10 +24,12 @@ import jakarta.servlet.FilterChain;
 | 
				
			|||||||
import jakarta.servlet.ServletException;
 | 
					import jakarta.servlet.ServletException;
 | 
				
			||||||
import jakarta.servlet.http.HttpServletRequest;
 | 
					import jakarta.servlet.http.HttpServletRequest;
 | 
				
			||||||
import jakarta.servlet.http.HttpServletResponse;
 | 
					import jakarta.servlet.http.HttpServletResponse;
 | 
				
			||||||
 | 
					import stirling.software.SPDF.model.Role;
 | 
				
			||||||
@Component
 | 
					@Component
 | 
				
			||||||
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
 | 
					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
 | 
					    @Autowired
 | 
				
			||||||
    private UserDetailsService userDetailsService;
 | 
					    private UserDetailsService userDetailsService;
 | 
				
			||||||
@ -39,7 +42,6 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
 | 
				
			|||||||
    protected void doFilterInternal(HttpServletRequest request,
 | 
					    protected void doFilterInternal(HttpServletRequest request,
 | 
				
			||||||
                                    HttpServletResponse response,
 | 
					                                    HttpServletResponse response,
 | 
				
			||||||
                                    FilterChain filterChain) throws ServletException, IOException {
 | 
					                                    FilterChain filterChain) throws ServletException, IOException {
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!rateLimit) {
 | 
					        if (!rateLimit) {
 | 
				
			||||||
            // If rateLimit is not enabled, just pass all requests without rate limiting
 | 
					            // If rateLimit is not enabled, just pass all requests without rate limiting
 | 
				
			||||||
            filterChain.doFilter(request, response);
 | 
					            filterChain.doFilter(request, response);
 | 
				
			||||||
@ -47,7 +49,6 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        String method = request.getMethod();
 | 
					        String method = request.getMethod();
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!"POST".equalsIgnoreCase(method)) {
 | 
					        if (!"POST".equalsIgnoreCase(method)) {
 | 
				
			||||||
            // If the request is not a POST, just pass it through without rate limiting
 | 
					            // If the request is not a POST, just pass it through without rate limiting
 | 
				
			||||||
            filterChain.doFilter(request, response);
 | 
					            filterChain.doFilter(request, response);
 | 
				
			||||||
@ -73,7 +74,34 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
 | 
				
			|||||||
            identifier = request.getRemoteAddr();
 | 
					            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);
 | 
					        ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (probe.isConsumed()) {
 | 
					        if (probe.isConsumed()) {
 | 
				
			||||||
@ -84,12 +112,11 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
 | 
				
			|||||||
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
 | 
					            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
 | 
				
			||||||
            response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
 | 
					            response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
 | 
				
			||||||
            response.getWriter().write("Rate limit exceeded for POST requests.");
 | 
					            response.getWriter().write("Rate limit exceeded for POST requests.");
 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private Bucket createUserBucket() {
 | 
					    private Bucket createUserBucket(int limitPerDay) {
 | 
				
			||||||
        Bandwidth limit = Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofDays(1)));
 | 
					        Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
 | 
				
			||||||
        return Bucket.builder().addLimit(limit).build();
 | 
					        return Bucket.builder().addLimit(limit).build();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -19,9 +19,10 @@ public class InitialSetup {
 | 
				
			|||||||
            String initialPassword = System.getenv("INITIAL_PASSWORD");
 | 
					            String initialPassword = System.getenv("INITIAL_PASSWORD");
 | 
				
			||||||
            if(initialUsername != null && initialPassword != null) {
 | 
					            if(initialUsername != null && initialPassword != null) {
 | 
				
			||||||
                userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
 | 
					                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;
 | 
					        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">
 | 
										            <div class="input-group mb-3">
 | 
				
			||||||
					                <input type="password" class="form-control" id="apiKey" placeholder="Your API Key" readonly>
 | 
										                <input type="password" class="form-control" id="apiKey" placeholder="Your API Key" readonly>
 | 
				
			||||||
					                <div class="input-group-append">
 | 
										                <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="showBtn" type="button" onclick="showApiKey()">👁️ Show</button>
 | 
				
			||||||
					                    <button class="btn btn-outline-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()">🔄 Refresh</button>
 | 
										                    <button class="btn btn-outline-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()">🔄 Refresh</button>
 | 
				
			||||||
 | 
										                    
 | 
				
			||||||
					                </div>
 | 
										                </div>
 | 
				
			||||||
					            </div>
 | 
										            </div>
 | 
				
			||||||
					        </div>
 | 
										        </div>
 | 
				
			||||||
					    </div>
 | 
										    </div>
 | 
				
			||||||
					    
 | 
										    
 | 
				
			||||||
					    <script>
 | 
										    <script>
 | 
				
			||||||
 | 
										    function copyToClipboard() {
 | 
				
			||||||
 | 
										        const apiKeyElement = document.getElementById("apiKey");
 | 
				
			||||||
 | 
										        apiKeyElement.select();
 | 
				
			||||||
 | 
										        document.execCommand("copy");
 | 
				
			||||||
 | 
										    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					    
 | 
										    
 | 
				
			||||||
					    function showApiKey() {
 | 
										    function showApiKey() {
 | 
				
			||||||
					        const apiKeyElement = document.getElementById("apiKey");
 | 
										        const apiKeyElement = document.getElementById("apiKey");
 | 
				
			||||||
 | 
										        const copyBtn = document.getElementById("copyBtn");
 | 
				
			||||||
					        if (apiKeyElement.type === "password") {
 | 
										        if (apiKeyElement.type === "password") {
 | 
				
			||||||
					            apiKeyElement.type = "text";
 | 
										        	apiKeyElement.type = "text";
 | 
				
			||||||
 | 
										        	copyBtn.disabled = false;  // Enable copy button when API key is visible
 | 
				
			||||||
					        } else {
 | 
										        } else {
 | 
				
			||||||
					            apiKeyElement.type = "password";
 | 
										            apiKeyElement.type = "password";
 | 
				
			||||||
 | 
										            copyBtn.disabled = true;  // Disable copy button when API key is hidden
 | 
				
			||||||
					        }
 | 
										        }
 | 
				
			||||||
					    }
 | 
										    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -107,6 +120,7 @@
 | 
				
			|||||||
					                let apiKey = await response.text();
 | 
										                let apiKey = await response.text();
 | 
				
			||||||
					                manageUIState(apiKey);
 | 
										                manageUIState(apiKey);
 | 
				
			||||||
					                document.getElementById("apiKey").type = 'text';
 | 
										                document.getElementById("apiKey").type = 'text';
 | 
				
			||||||
 | 
										                document.getElementById("copyBtn").disabled = false;
 | 
				
			||||||
					            } else {
 | 
										            } else {
 | 
				
			||||||
					                alert('Error refreshing API key.');
 | 
										                alert('Error refreshing API key.');
 | 
				
			||||||
					            }
 | 
										            }
 | 
				
			||||||
@ -118,13 +132,16 @@
 | 
				
			|||||||
					    function manageUIState(apiKey) {
 | 
										    function manageUIState(apiKey) {
 | 
				
			||||||
					        const apiKeyElement = document.getElementById("apiKey");
 | 
										        const apiKeyElement = document.getElementById("apiKey");
 | 
				
			||||||
					        const showBtn = document.getElementById("showBtn");
 | 
										        const showBtn = document.getElementById("showBtn");
 | 
				
			||||||
 | 
										        const copyBtn = document.getElementById("copyBtn");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					        if (apiKey && apiKey.trim().length > 0) {
 | 
										        if (apiKey && apiKey.trim().length > 0) {
 | 
				
			||||||
					            apiKeyElement.value = apiKey;
 | 
										            apiKeyElement.value = apiKey;
 | 
				
			||||||
					            showBtn.disabled = false;
 | 
										            showBtn.disabled = false;
 | 
				
			||||||
 | 
										            copyBtn.disabled = true;  
 | 
				
			||||||
					        } else {
 | 
										        } else {
 | 
				
			||||||
					            apiKeyElement.value = "";
 | 
										            apiKeyElement.value = "";
 | 
				
			||||||
					            showBtn.disabled = true;
 | 
										            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>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html xmlns:th="http://www.thymeleaf.org">
 | 
					<html xmlns:th="http://www.thymeleaf.org">
 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user