diff --git a/src/main/java/iz/_11a/passmetric/PasswordController.java b/src/main/java/iz/_11a/passmetric/PasswordController.java index d61f2c5..a604dea 100644 --- a/src/main/java/iz/_11a/passmetric/PasswordController.java +++ b/src/main/java/iz/_11a/passmetric/PasswordController.java @@ -3,6 +3,7 @@ package iz._11a.passmetric; import iz._11a.passmetric.model.PasswordLeakResult; import iz._11a.passmetric.service.BruteForceService; import iz._11a.passmetric.service.PasswordLeakService; +import iz._11a.passmetric.service.PasswordStatisticsService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @@ -24,7 +25,7 @@ public class PasswordController { @PostMapping(path = "/api/password/strength") @ResponseBody - public Map checkPasswordLive(@RequestParam("password") String password) { + public Map checkPasswordLive(@RequestBody String password) { Map response = new HashMap<>(); @@ -38,6 +39,7 @@ public class PasswordController { response.put("tips", tips); response.put("leaked", leakResult.isLeaked() ? "Hasło wyciekło " + leakResult.getCount() +" razy" : "Hasło nie występuje w wyciekach"); response.put("timetohack", BruteForceService.estimateTimeToHackFormatted(password)); + response.put("stats", PasswordStatisticsService.getPasswordStatistics(password)); return response; } diff --git a/src/main/java/iz/_11a/passmetric/service/PasswordStatisticsService.java b/src/main/java/iz/_11a/passmetric/service/PasswordStatisticsService.java new file mode 100644 index 0000000..c3d019d --- /dev/null +++ b/src/main/java/iz/_11a/passmetric/service/PasswordStatisticsService.java @@ -0,0 +1,45 @@ +package iz._11a.passmetric.service; + +import java.util.HashMap; +import java.util.Map; + +public class PasswordStatisticsService { + + public static Map getPasswordStatistics(String password) { + Map stats = new HashMap<>(); + + if (password == null || password.isEmpty()) { + stats.put("length", 0); + stats.put("lowercaseCount", 0); + stats.put("uppercaseCount", 0); + stats.put("digitCount", 0); + stats.put("specialCount", 0); + return stats; + } + + int lowercaseCount = 0; + int uppercaseCount = 0; + int digitCount = 0; + int specialCount = 0; + + for (char c : password.toCharArray()) { + if (Character.isLowerCase(c)) { + lowercaseCount++; + } else if (Character.isUpperCase(c)) { + uppercaseCount++; + } else if (Character.isDigit(c)) { + digitCount++; + } else { + specialCount++; + } + } + + stats.put("length", password.length()); + stats.put("lowercaseCount", lowercaseCount); + stats.put("uppercaseCount", uppercaseCount); + stats.put("digitCount", digitCount); + stats.put("specialCount", specialCount); + + return stats; + } +} diff --git a/src/main/resources/static/css/password.css b/src/main/resources/static/css/password.css index 120bf17..9318c01 100644 --- a/src/main/resources/static/css/password.css +++ b/src/main/resources/static/css/password.css @@ -219,3 +219,68 @@ ul#tipsList li { margin: 0.3rem 0.5rem; } +/*stats*/ + +.stats-container { + width: min(92vw, 540px); + margin: 16px auto 0; + padding: 16px; + border-radius: 12px; + border: 1.5px solid var(--border); + background: var(--panel); + backdrop-filter: blur(12px); + display: none; +} + +.stats-title { + margin: 0 0 12px; + font-size: 15px; + font-weight: 600; + color: var(--text); + text-align: center; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 10px; +} + +.stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: rgba(148, 163, 184, 0.1); + border-radius: 8px; + transition: background 160ms ease; +} + +@media (prefers-color-scheme: dark) { + .stat-item { + background: rgba(51, 65, 85, 0.4); + } +} + +.stat-item:hover { + background: rgba(148, 163, 184, 0.15); +} + +@media (prefers-color-scheme: dark) { + .stat-item:hover { + background: rgba(51, 65, 85, 0.6); + } +} + +.stat-label { + font-size: 13px; + color: var(--muted); + font-weight: 500; +} + +.stat-value { + font-size: 14px; + font-weight: 700; + color: var(--primary); +} + diff --git a/src/main/resources/templates/password.html b/src/main/resources/templates/password.html index 9f80040..a259689 100644 --- a/src/main/resources/templates/password.html +++ b/src/main/resources/templates/password.html @@ -19,6 +19,33 @@
+
+

Statystyki hasła

+
+
+ Długość: + - +
+
+ Małe litery: + - +
+
+ Wielkie litery: + - +
+
+ Cyfry: + - +
+
+ Znaki specjalne: + - +
+
+
+ +

@@ -36,6 +63,7 @@ const tipsList = document.getElementById('tipsList'); const leakOut = document.getElementById('leakMessage'); const timeToHackOut = document.getElementById('timeToHackMessage'); + const statsContainer = document.getElementById('statsContainer'); // ukryj przy pierwszym załadowaniu strony tipsList.style.display = 'none'; @@ -56,6 +84,7 @@ tipsList.innerHTML = ""; tipsList.style.display = 'none'; timeToHackOut.textContent = ''; + statsContainer.style.display = 'none'; return; } @@ -63,8 +92,8 @@ try { const resp = await fetch('/api/password/strength', { method: 'POST', - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: new URLSearchParams({password: val}) + headers: {'Content-Type': 'text/plain'}, + body: val, }); if (!resp.ok) { out.textContent = 'Błąd sprawdzania'; @@ -140,6 +169,23 @@ timeToHackOut.classList.remove("ok"); } + // stats + if (data.stats) { + document.getElementById('statLength').textContent = data.stats.length || 0; + document.getElementById('statLowercase').textContent = + data.stats.lowercaseCount || 0; + document.getElementById('statUppercase').textContent = + data.stats.uppercaseCount || 0; + document.getElementById('statDigits').textContent = + data.stats.digitCount || 0; + document.getElementById('statSpecial').textContent = + data.stats.specialCount || 0; + + statsContainer.style.display = 'block'; + } else { + statsContainer.style.display = 'none'; + } + } catch { out.textContent = 'Błąd sieci'; leakOut.textContent = '';