Skip to content

Commit 433209f

Browse files
committed
Groundwork for #398
1 parent cd2fa1a commit 433209f

8 files changed

Lines changed: 338 additions & 0 deletions

File tree

client/src/api/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import I18n from "../locale/I18n";
22
import {useAppStore} from "../stores/AppStore";
33
import {paginationQueryParams} from "../utils/Pagination.js";
4+
import {isEmpty} from "../utils/Utils.js";
45

56
//Internal API
67
function validateResponse(showErrorDialog) {
@@ -334,3 +335,19 @@ export function publicServiceProviders() {
334335
export function publicServiceProviderByDetail(type, identifier) {
335336
return fetchJson(`/api/v1/public/service-provider-detail/${type}/${identifier}`);
336337
}
338+
339+
//Stats
340+
export function loginTimeFrame(from, to, scale, spEntityId) {
341+
const sp = !isEmpty(spEntityId) ? `&spEntityId=${encodeURIComponent(spEntityId)}` : ''
342+
return fetchJson(`/api/v1/stats/loginTimeFrame?from=${from}&to=${to}&scale=${scale}&${sp}`)
343+
}
344+
345+
export function loginAggregated(period, spEntityId) {
346+
const sp = !isEmpty(spEntityId) ? `&spEntityId=${encodeURIComponent(spEntityId)}` : ''
347+
return fetchJson(`/api/v1/stats/loginAggregated?period=${period}${sp}`)
348+
}
349+
350+
export function uniqueLoginCount(from, to, spEntityId) {
351+
return fetchJson(`/api/v1/stats/uniqueLoginCount?from=${from}&to=${to}&spEntityId=${encodeURIComponent(spEntityId)}`)
352+
}
353+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from "react";
2+
import "./Statistics.scss"
3+
4+
export const Statistics = () => {
5+
6+
return (
7+
<div className="statistics-container">
8+
<div className="statistics">
9+
<p>react-chartjs-2</p>
10+
</div>
11+
</div>
12+
);
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.statistics-container {
2+
width: 100%;
3+
display: flex;
4+
flex-direction: column;
5+
6+
.statistics {
7+
padding: 25px;
8+
}
9+
10+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package access.api;
2+
3+
import access.model.User;
4+
import access.stats.Statistics;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RequestParam;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
import java.util.List;
12+
13+
@RestController
14+
@RequestMapping("/api/v1/stats")
15+
public class StatisticsController {
16+
17+
private Statistics statistics;
18+
19+
@Autowired
20+
public StatisticsController(Statistics statistics) {
21+
this.statistics = statistics;
22+
}
23+
24+
//Used for retrieval of all logins for one SP
25+
@GetMapping("loginTimeFrame")
26+
public List<Object> loginTimeFrame(User user,
27+
@RequestParam("from") long from,
28+
@RequestParam("to") long to,
29+
@RequestParam("scale") String scale,
30+
@RequestParam(value = "spEntityId", required = false) String spEntityId) {
31+
String authenticatingAuthority = user.getAuthenticatingAuthority();
32+
return statistics.loginTimeFrame(from, to, scale, authenticatingAuthority, spEntityId);
33+
}
34+
35+
//Used for retrieval of all logins for all SPs
36+
@GetMapping("loginAggregated")
37+
public List<Object> loginAggregated(User user,
38+
@RequestParam("period") String period,
39+
@RequestParam(value = "spEntityId", required = false) String spEntityId) {
40+
String authenticatingAuthority = user.getAuthenticatingAuthority();
41+
return statistics.loginAggregated(period, authenticatingAuthority, spEntityId);
42+
}
43+
44+
//Used for retrieval of all logins for one SP without a period
45+
@GetMapping("uniqueLoginCount")
46+
public List<Object> uniqueLoginCount(User user,
47+
@RequestParam("from") long from,
48+
@RequestParam("to") long to,
49+
@RequestParam(value = "spEntityId") String spEntityId) {
50+
String authenticatingAuthority = user.getAuthenticatingAuthority();
51+
return statistics.uniqueLoginCount(from, to, authenticatingAuthority, spEntityId);
52+
}
53+
54+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package access.stats;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
6+
public interface Statistics {
7+
8+
List<Object> loginTimeFrame(long from, long to, String scale, String idpEntityId, String spEntityId);
9+
10+
List<Object> loginAggregated(String period, String idpEntityId, String spEntityId);
11+
12+
List<Object> uniqueLoginCount(long from, long to, String idpEntityId, String spEntityId);
13+
14+
}
15+
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package access.stats;
2+
3+
import access.manage.Manage;
4+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.util.StringUtils;
7+
8+
import java.util.*;
9+
import java.util.stream.Collectors;
10+
11+
import static access.manage.ManageData.getData;
12+
13+
@SuppressWarnings("unchecked")
14+
@Component
15+
@ConditionalOnProperty(
16+
prefix = "statistics",
17+
name = "enabled",
18+
havingValue = "false"
19+
)
20+
public class StatisticsMock implements Statistics {
21+
22+
private final Manage manage;
23+
24+
public StatisticsMock(Manage manage) {
25+
this.manage = manage;
26+
}
27+
28+
@Override
29+
public List<Object> loginTimeFrame(long from, long to, String scale, String idpEntityId, String spEntityId) {
30+
long step = step(scale);
31+
List<Object> result = new ArrayList<>();
32+
for (long i = from; i <= to; i += step) {
33+
Map<String, Object> point = new HashMap<>();
34+
point.put("count_user_id", countValue(scale));
35+
if (!"minute".equals(scale) && !"hour".equals(scale)) {
36+
point.put("distinct_count_user_id", countValue(scale));
37+
}
38+
if (StringUtils.hasText(spEntityId)) {
39+
point.put("sp_entity_id", spEntityId);
40+
}
41+
point.put("idp_entity_id", idpEntityId);
42+
point.put("time", i * 1000);
43+
result.add(point);
44+
}
45+
return result;
46+
}
47+
48+
@Override
49+
public List<Object> loginAggregated(String period, String idpEntityId, String spEntityId) {
50+
Calendar today = Calendar.getInstance();
51+
today.set(Calendar.YEAR, Integer.valueOf(period.substring(0, 4)));
52+
today.set(Calendar.HOUR_OF_DAY, 0);
53+
today.set(Calendar.MINUTE, 0);
54+
today.set(Calendar.DAY_OF_MONTH, 1);
55+
if (period.length() > 4) {
56+
switch (period.substring(4, 5).toUpperCase()) {
57+
case "Q": {
58+
today.set(Calendar.MONTH, ((Integer.valueOf(period.substring(5)) - 1) * 3));
59+
}
60+
case "M": {
61+
today.set(Calendar.MONTH, Integer.valueOf(period.substring(5)) - 1);
62+
}
63+
case "W": {
64+
today.set(Calendar.WEEK_OF_YEAR, Integer.valueOf(period.substring(5)));
65+
}
66+
case "D": {
67+
today.set(Calendar.DAY_OF_YEAR, Integer.valueOf(period.substring(5)));
68+
}
69+
}
70+
} else {
71+
today.set(Calendar.MONTH, 0);
72+
}
73+
long date = today.getTimeInMillis() / 1000;
74+
Map<String, Object> identityProvider = manage.identityProviderByEntityID(idpEntityId);
75+
Map<String, Object> data = getData(identityProvider);
76+
List<String> entityIdentifiers = ((List<Map<String, String>>) data.getOrDefault("allowedEntities", List.of()))
77+
.stream()
78+
.map(sp -> (String) sp.get("name"))
79+
.toList();
80+
81+
82+
List<Map<String, Object>> serviceProvider = manage.serviceProvidersByEntityID(entityIdentifiers);
83+
return serviceProvider.stream()
84+
.filter(sp -> !StringUtils.hasText(spEntityId) || spEntityId.equals(getData(sp).get("entityid")))
85+
.map(sp -> {
86+
Map<String, Object> point = new HashMap<>();
87+
point.put("count_user_id", countValue(periodToScale(period)));
88+
point.put("distinct_count_user_id", countValue(periodToScale(period)) / 2);
89+
point.put("sp_entity_id", getData(sp).get("entityid"));
90+
point.put("idp_entity_id", idpEntityId);
91+
point.put("time", date);
92+
return point;
93+
}).collect(Collectors.toList());
94+
}
95+
96+
@Override
97+
public List<Object> uniqueLoginCount(long from, long to, String idpEntityId, String spEntityId) {
98+
List<Object> result = new ArrayList<>();
99+
Map<String, Object> point = new HashMap<>();
100+
point.put("count_user_id", countValue("year"));
101+
point.put("sp_entity_id", spEntityId);
102+
point.put("idp_entity_id", idpEntityId);
103+
point.put("time", from);
104+
result.add(point);
105+
return result;
106+
}
107+
108+
private String periodToScale(String period) {
109+
if (period.length() == 4) {
110+
return "year";
111+
}
112+
switch (period.substring(4, 5).toLowerCase()) {
113+
case "d":
114+
return "day";
115+
case "w":
116+
return "week";
117+
case "m":
118+
return "month";
119+
case "q":
120+
return "quarter";
121+
default:
122+
throw new IllegalArgumentException("Unknown period:" + period);
123+
}
124+
}
125+
126+
private long countValue(String scale) {
127+
double base = Math.floor(10000 * (Math.random() + 1));
128+
return (long) base;
129+
}
130+
131+
private long doSwitch(String scale, long base) {
132+
switch (scale) {
133+
case "minute":
134+
return base * 60;
135+
case "hour":
136+
return base * 60 * 60;
137+
case "day":
138+
return base * 24 * 60 * 60;
139+
case "week":
140+
return base * 7 * 24 * 60 * 60;
141+
case "month":
142+
return base * 30 * 24 * 60 * 60;
143+
case "quarter":
144+
return base * 90 * 24 * 60 * 60;
145+
case "year":
146+
return base * 365 * 24 * 60 * 60;
147+
default:
148+
throw new IllegalArgumentException("Unknown scale:" + scale);
149+
}
150+
}
151+
152+
private long step(String scale) {
153+
return doSwitch(scale, 1L);
154+
}
155+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package access.stats;
2+
3+
import access.remote.RestTemplateFactory;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.util.StringUtils;
9+
import org.springframework.web.client.RestTemplate;
10+
11+
import java.net.URLEncoder;
12+
import java.nio.charset.Charset;
13+
import java.util.List;
14+
15+
@SuppressWarnings("unchecked")
16+
@Component
17+
@ConditionalOnProperty(
18+
prefix = "statistics",
19+
name = "enabled",
20+
havingValue = "true"
21+
)
22+
public class StatisticsRemote implements Statistics {
23+
24+
private final RestTemplate restTemplate;
25+
private final String baseUrl;
26+
27+
@Autowired
28+
public StatisticsRemote(@Value("${statsUser}") String user,
29+
@Value("${statsPassword}") String password,
30+
@Value("${statsBaseUrl}") String baseUrl) {
31+
this.restTemplate = RestTemplateFactory.buildRestTemplate(user, password);
32+
this.baseUrl = baseUrl;
33+
}
34+
35+
public List<Object> loginTimeFrame(long from, long to, String scale, String idpEntityId, String spEntityId) {
36+
String idp = encodeEntityID(idpEntityId);
37+
StringBuilder url = new StringBuilder(String.format(
38+
"%s/public/login_time_frame?from=%s&to=%s&include_unique=true&scale=%s&epoch=ms&idp_id=%s",
39+
baseUrl, from, to, scale, idp));
40+
if (StringUtils.hasText(spEntityId)) {
41+
url.append(String.format("&sp_id=%s", encodeEntityID(spEntityId)));
42+
}
43+
return restTemplate.getForEntity(url.toString(), List.class).getBody();
44+
}
45+
46+
private String encodeEntityID(String entityID) {
47+
return URLEncoder.encode(entityID, Charset.defaultCharset());
48+
}
49+
50+
public List<Object> loginAggregated(String period, String idpEntityId, String spEntityId) {
51+
StringBuilder url = new StringBuilder(String.format(
52+
"%s/public/login_aggregated?period=%s&include_unique=true&idp_id=%s&group_by=sp_id",
53+
baseUrl, period, encodeEntityID(idpEntityId)));
54+
if (StringUtils.hasText(spEntityId)) {
55+
url.append(String.format("&sp_id=%s", encodeEntityID(spEntityId)));
56+
}
57+
return restTemplate.getForEntity(url.toString(), List.class).getBody();
58+
}
59+
60+
public List<Object> uniqueLoginCount(long from, long to, String idpEntityId, String spEntityId) {
61+
String url = String.format(
62+
"%s/public/unique_login_count?from=%s&to=%s&include_unique=true&epoch=ms&idp_id=%s&sp_id=%s",
63+
baseUrl, from, to, encodeEntityID(idpEntityId), encodeEntityID(spEntityId));
64+
return restTemplate.getForEntity(url, List.class).getBody();
65+
}
66+
67+
}
68+

server/src/main/resources/application.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ invite:
181181
user: access
182182
password: secret
183183

184+
statistics:
185+
enabled: False
186+
url: "http://locallhost:8081"
187+
user: access
188+
password: secret
189+
184190
s3storage:
185191
# url: "https://minioapi.test2.surfconext.nl"
186192
# key: "minioadmin"

0 commit comments

Comments
 (0)