Merge branch 'master' into change-to-polymorphie
This commit is contained in:
@ -60,8 +60,10 @@ dependencies {
|
|||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
|
||||||
|
compile group: 'org.springframework.boot', name: 'spring-boot-starter-oauth2-client', version: '2.2.5.RELEASE'
|
||||||
|
|
||||||
implementation 'org.keycloak:keycloak-spring-boot-starter:9.0.0'
|
implementation 'org.keycloak:keycloak-spring-boot-starter:9.0.0'
|
||||||
implementation 'org.keycloak.bom:keycloak-adapter-bom:3.3.0.Final'
|
implementation 'org.keycloak.bom:keycloak-adapter-bom:9.0.0'
|
||||||
implementation 'mops:styleguide:2.1.0'
|
implementation 'mops:styleguide:2.1.0'
|
||||||
implementation 'io.springfox:springfox-swagger2:2.9.2'
|
implementation 'io.springfox:springfox-swagger2:2.9.2'
|
||||||
implementation 'io.springfox:springfox-swagger-ui:2.9.2'
|
implementation 'io.springfox:springfox-swagger-ui:2.9.2'
|
||||||
@ -73,6 +75,8 @@ dependencies {
|
|||||||
runtimeOnly 'com.h2database:h2'
|
runtimeOnly 'com.h2database:h2'
|
||||||
runtimeOnly 'mysql:mysql-connector-java'
|
runtimeOnly 'mysql:mysql-connector-java'
|
||||||
|
|
||||||
|
compile group: 'org.springframework.security.oauth', name: 'spring-security-oauth2', version: '2.4.0.RELEASE'
|
||||||
|
|
||||||
testImplementation 'org.assertj:assertj-core:3.15.0'
|
testImplementation 'org.assertj:assertj-core:3.15.0'
|
||||||
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
||||||
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
|
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
|
||||||
@ -80,6 +84,7 @@ dependencies {
|
|||||||
testImplementation 'org.springframework.security:spring-security-test'
|
testImplementation 'org.springframework.security:spring-security-test'
|
||||||
testImplementation 'com.tngtech.archunit:archunit-junit5:0.13.1'
|
testImplementation 'com.tngtech.archunit:archunit-junit5:0.13.1'
|
||||||
implementation 'junit:junit:4.12'
|
implementation 'junit:junit:4.12'
|
||||||
|
implementation 'junit:junit:4.12'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import mops.gruppen2.service.APIFormatterService;
|
|||||||
import mops.gruppen2.service.EventService;
|
import mops.gruppen2.service.EventService;
|
||||||
import mops.gruppen2.service.GroupService;
|
import mops.gruppen2.service.GroupService;
|
||||||
import mops.gruppen2.service.SerializationService;
|
import mops.gruppen2.service.SerializationService;
|
||||||
|
import org.springframework.security.access.annotation.Secured;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -33,6 +34,7 @@ public class APIController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/updateGroups/{status}")
|
@GetMapping("/updateGroups/{status}")
|
||||||
|
@Secured("ROLE_api_user")
|
||||||
@ApiOperation(value = "Gibt alle Gruppen zurück in denen sich etwas geändert hat")
|
@ApiOperation(value = "Gibt alle Gruppen zurück in denen sich etwas geändert hat")
|
||||||
public UpdatedGroupRequestMapper updateGroup(@ApiParam("Letzter Status des Anfragestellers") @PathVariable Long status) throws EventException {
|
public UpdatedGroupRequestMapper updateGroup(@ApiParam("Letzter Status des Anfragestellers") @PathVariable Long status) throws EventException {
|
||||||
List<Event> events = eventService.getNewEvents(status);
|
List<Event> events = eventService.getNewEvents(status);
|
||||||
@ -42,12 +44,14 @@ public class APIController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/getGroupIdsOfUser/{teilnehmer}")
|
@GetMapping("/getGroupIdsOfUser/{teilnehmer}")
|
||||||
|
@Secured("ROLE_api_user")
|
||||||
@ApiOperation(value = "Gibt alle Gruppen zurück in denen sich ein Teilnehmer befindet")
|
@ApiOperation(value = "Gibt alle Gruppen zurück in denen sich ein Teilnehmer befindet")
|
||||||
public List<Long> getGroupsOfUser(@ApiParam("Teilnehmer dessen groupIds zurückgegeben werden sollen") @PathVariable String teilnehmer) throws EventException {
|
public List<Long> getGroupsOfUser(@ApiParam("Teilnehmer dessen groupIds zurückgegeben werden sollen") @PathVariable String teilnehmer) throws EventException {
|
||||||
return eventService.getGroupsOfUser(teilnehmer);
|
return eventService.getGroupsOfUser(teilnehmer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/getGroup/{groupId}")
|
@GetMapping("/getGroup/{groupId}")
|
||||||
|
@Secured("ROLE_api_user")
|
||||||
@ApiOperation(value = "Gibt die Gruppe mit der als Parameter mitgegebenden groupId zurück")
|
@ApiOperation(value = "Gibt die Gruppe mit der als Parameter mitgegebenden groupId zurück")
|
||||||
public Group getGroupFromId(@ApiParam("GruppenId der gefordeten Gruppe") @PathVariable Long groupId) throws EventException{
|
public Group getGroupFromId(@ApiParam("GruppenId der gefordeten Gruppe") @PathVariable Long groupId) throws EventException{
|
||||||
List<Event> eventList = eventService.getEventsOfGroup(groupId);
|
List<Event> eventList = eventService.getEventsOfGroup(groupId);
|
||||||
|
|||||||
@ -3,8 +3,7 @@ package mops.gruppen2.controller;
|
|||||||
import mops.gruppen2.config.Gruppen2Config;
|
import mops.gruppen2.config.Gruppen2Config;
|
||||||
import mops.gruppen2.domain.Exceptions.EventException;
|
import mops.gruppen2.domain.Exceptions.EventException;
|
||||||
import mops.gruppen2.domain.Group;
|
import mops.gruppen2.domain.Group;
|
||||||
import mops.gruppen2.domain.GroupType;
|
|
||||||
import mops.gruppen2.domain.Role;
|
|
||||||
import mops.gruppen2.domain.User;
|
import mops.gruppen2.domain.User;
|
||||||
import mops.gruppen2.domain.event.CreateGroupEvent;
|
import mops.gruppen2.domain.event.CreateGroupEvent;
|
||||||
import mops.gruppen2.security.Account;
|
import mops.gruppen2.security.Account;
|
||||||
@ -15,12 +14,12 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import javax.annotation.security.RolesAllowed;
|
import javax.annotation.security.RolesAllowed;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/gruppen2")
|
@RequestMapping("/gruppen2")
|
||||||
@ -114,4 +113,16 @@ public class Gruppen2Controller {
|
|||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Group not found");
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Group not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator)"})
|
||||||
|
@GetMapping("/detailsSearch")
|
||||||
|
public String showGroupDetailsNoMember (KeycloakAuthenticationToken token, Model model, @RequestParam (value="id") Long id) throws EventException {
|
||||||
|
model.addAttribute("account", keyCloakService.createAccountFromPrincipal(token));
|
||||||
|
Group group = userService.getGroupById(id);
|
||||||
|
if (group!=null) {
|
||||||
|
model.addAttribute("group", group);
|
||||||
|
return "detailsNoMember";
|
||||||
|
}
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Group not found");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
package mops.gruppen2.security;
|
package mops.gruppen2.security;
|
||||||
|
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
|
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
|
||||||
|
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WORKAROUND for https://issues.redhat.com/browse/KEYCLOAK-11282
|
* WORKAROUND for https://issues.redhat.com/browse/KEYCLOAK-11282
|
||||||
@ -15,4 +20,35 @@ public class KeycloakConfig {
|
|||||||
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
|
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
|
||||||
return new KeycloakSpringBootConfigResolver();
|
return new KeycloakSpringBootConfigResolver();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Value("${keycloak.resource}")
|
||||||
|
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
@Value("${keycloak.credentials.secret}")
|
||||||
|
|
||||||
|
private String clientSecret;
|
||||||
|
|
||||||
|
@Value("${hhu_keycloak.token-uri}")
|
||||||
|
|
||||||
|
private String tokenUri;
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate serviceAccountRestTemplate() {
|
||||||
|
|
||||||
|
ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
|
||||||
|
|
||||||
|
resourceDetails.setGrantType(OAuth2Constants.CLIENT_CREDENTIALS);
|
||||||
|
|
||||||
|
resourceDetails.setAccessTokenUri(tokenUri);
|
||||||
|
|
||||||
|
resourceDetails.setClientId(clientId);
|
||||||
|
|
||||||
|
resourceDetails.setClientSecret(clientSecret);
|
||||||
|
|
||||||
|
|
||||||
|
return new OAuth2RestTemplate(resourceDetails);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,8 +2,6 @@ package mops.gruppen2.service;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import mops.gruppen2.domain.EventDTO;
|
import mops.gruppen2.domain.EventDTO;
|
||||||
import mops.gruppen2.domain.Exceptions.EventException;
|
|
||||||
import mops.gruppen2.domain.Group;
|
|
||||||
import mops.gruppen2.domain.Visibility;
|
import mops.gruppen2.domain.Visibility;
|
||||||
import mops.gruppen2.domain.event.CreateGroupEvent;
|
import mops.gruppen2.domain.event.CreateGroupEvent;
|
||||||
import mops.gruppen2.domain.event.Event;
|
import mops.gruppen2.domain.event.Event;
|
||||||
@ -23,7 +21,8 @@ public class EventService {
|
|||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** sichert ein Event Objekt indem es ein EventDTO Objekt erzeugt
|
/**
|
||||||
|
* sichert ein Event Objekt indem es ein EventDTO Objekt erzeugt
|
||||||
*
|
*
|
||||||
* @param event
|
* @param event
|
||||||
*/
|
*/
|
||||||
@ -32,7 +31,8 @@ public class EventService {
|
|||||||
eventStore.save(eventDTO);
|
eventStore.save(eventDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Erzeugt aus einem Event Objekt ein EventDTO Objekt.
|
/**
|
||||||
|
* Erzeugt aus einem Event Objekt ein EventDTO Objekt.
|
||||||
* Ist die Gruppe öffentlich, dann wird die visibility auf true gesetzt.
|
* Ist die Gruppe öffentlich, dann wird die visibility auf true gesetzt.
|
||||||
*
|
*
|
||||||
* @param event
|
* @param event
|
||||||
@ -50,7 +50,6 @@ public class EventService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
eventDTO.setEvent_payload(serializationService.serializeEvent(event));
|
eventDTO.setEvent_payload(serializationService.serializeEvent(event));
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
@ -59,7 +58,8 @@ public class EventService {
|
|||||||
return eventDTO;
|
return eventDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sorgt dafür die Group_id immer um 1 zu erhöhen
|
/**
|
||||||
|
* Sorgt dafür die Group_id immer um 1 zu erhöhen
|
||||||
*
|
*
|
||||||
* @return Gibt Long zurück
|
* @return Gibt Long zurück
|
||||||
*/
|
*/
|
||||||
@ -77,7 +77,8 @@ public class EventService {
|
|||||||
return tmpId;
|
return tmpId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Findet alle Events welche ab dem neuen Status hinzugekommen sind
|
/**
|
||||||
|
* Findet alle Events welche ab dem neuen Status hinzugekommen sind
|
||||||
*
|
*
|
||||||
* @param status
|
* @param status
|
||||||
* @return Liste von Events
|
* @return Liste von Events
|
||||||
@ -89,7 +90,8 @@ public class EventService {
|
|||||||
return translateEventDTOs(groupEventDTOS);
|
return translateEventDTOs(groupEventDTOS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Erzeugt aus einer Liste von eventDTOs eine Liste von Events
|
/**
|
||||||
|
* Erzeugt aus einer Liste von eventDTOs eine Liste von Events
|
||||||
*
|
*
|
||||||
* @param eventDTOS
|
* @param eventDTOS
|
||||||
* @return Liste von Events
|
* @return Liste von Events
|
||||||
|
|||||||
@ -12,5 +12,12 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
|||||||
keycloak.principal-attribute=preferred_username
|
keycloak.principal-attribute=preferred_username
|
||||||
keycloak.auth-server-url=https://keycloak.cs.hhu.de/auth
|
keycloak.auth-server-url=https://keycloak.cs.hhu.de/auth
|
||||||
keycloak.realm=MOPS
|
keycloak.realm=MOPS
|
||||||
keycloak.resource=demo
|
|
||||||
keycloak.public-client=true
|
hhu_keycloak.token-uri=https://keycloak.cs.hhu.de/auth/realms/MOPS/protocol/openid-connect/token
|
||||||
|
keycloak.resource=gruppenfindung
|
||||||
|
keycloak.credentials.secret= fc6ebf10-8c63-4e71-a667-4eae4e8209a1
|
||||||
|
keycloak.verify-token-audience=true
|
||||||
|
keycloak.use-resource-role-mappings=true
|
||||||
|
|
||||||
|
keycloak.autodetect-bearer-only=true
|
||||||
|
keycloak.confidential-port= 443
|
||||||
@ -10,5 +10,12 @@ spring.datasource.password=geheim
|
|||||||
keycloak.principal-attribute=preferred_username
|
keycloak.principal-attribute=preferred_username
|
||||||
keycloak.auth-server-url=https://keycloak.cs.hhu.de/auth
|
keycloak.auth-server-url=https://keycloak.cs.hhu.de/auth
|
||||||
keycloak.realm=MOPS
|
keycloak.realm=MOPS
|
||||||
keycloak.resource=demo
|
|
||||||
keycloak.public-client=true
|
hhu_keycloak.token-uri=https://keycloak.cs.hhu.de/auth/realms/MOPS/protocol/openid-connect/token
|
||||||
|
keycloak.resource=gruppenfindung
|
||||||
|
keycloak.credentials.secret= fc6ebf10-8c63-4e71-a667-4eae4e8209a1
|
||||||
|
keycloak.verify-token-audience=true
|
||||||
|
keycloak.use-resource-role-mappings=true
|
||||||
|
|
||||||
|
keycloak.autodetect-bearer-only=true
|
||||||
|
keycloak.confidential-port= 443
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
spring.profiles.active=dev
|
spring.profiles.active=dev
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,26 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main th:fragment="bodycontent">
|
<main th:fragment="bodycontent">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9" style="border: 10px solid aliceblue; background: aliceblue">
|
||||||
|
<form action="/" method="get">
|
||||||
|
<h1 style="color: dodgerblue; font-weight: bold" th:text="${group.getTitle()}"></h1>
|
||||||
|
<p style="font-weight: bold">
|
||||||
|
<span class="badge badge-pill badge-dark" style="background: darkslategray" th:if="${group.getVisibility() == group.getVisibility().PRIVATE }">Private Gruppe</span>
|
||||||
|
<span class="badge badge-pill badge-primary" th:if="${group.getVisibility() == group.getVisibility().PUBLIC}">Öffentliche Gruppe</span>
|
||||||
|
<span class="badge badge-pill badge-success" style="background: lightseagreen" th:if="${group.getType() == group.getType().LECTURE}"> Veranstaltung</span>
|
||||||
|
</p>
|
||||||
|
<p th:text="${group.getDescription()}"></p>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="text-right">
|
||||||
|
<button class="btn btn-primary" style="border-style: none;">Gruppe beitreten</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
26
src/main/resources/templates/errorRenameLater.html
Normal file
26
src/main/resources/templates/errorRenameLater.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Error</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="mx-auto" style="vertical-align: center; horiz-align: center; top: 50%; left: 50%; margin-top: 200px">
|
||||||
|
<div class="text-center" style="background: aliceblue; align-items: center; margin: auto; width: 1000px; vertical-align: center; padding: 50px; display: block">
|
||||||
|
<h1 style="text-align: center">Da ist etwas schiefgelaufen!</h1>
|
||||||
|
<h2 style="text-align: center">Die Seite, nach der du suchst, scheint nicht zu existieren.</h2>
|
||||||
|
<br>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-primary" style="margin: auto">
|
||||||
|
<a style="color: white" href="#" onclick="javascript:window.history.back(-1);return false;">Zurück zur letzten Seite</a>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -35,8 +35,6 @@
|
|||||||
<label for="suchleiste">Suchbegriff:</label>
|
<label for="suchleiste">Suchbegriff:</label>
|
||||||
<input id="suchleiste" class="form-control" placeholder="z.B. Programmieren, Lerngruppe, ..." th:name="suchbegriff" type="text">
|
<input id="suchleiste" class="form-control" placeholder="z.B. Programmieren, Lerngruppe, ..." th:name="suchbegriff" type="text">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary" style="background: #52a1eb; border-style: none">Suchen</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
<br>
|
<br>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
@ -51,7 +49,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody th:each="gruppe : ${gruppen}">
|
<tbody th:each="gruppe : ${gruppen}">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" th:text="${gruppe.title}">Gruppenname</th>
|
<th scope="row">
|
||||||
|
<a th:href="@{/gruppen2/detailsSearch(id=${gruppe.getId()})}" th:text="${gruppe.title}">Gruppenname</a>
|
||||||
|
</th>
|
||||||
<td th:text="${gruppe.getDescription()}">Beschreibung</td>
|
<td th:text="${gruppe.getDescription()}">Beschreibung</td>
|
||||||
<td th:text="${gruppe.getVisibility()}">Öffentlich</td>
|
<td th:text="${gruppe.getVisibility()}">Öffentlich</td>
|
||||||
<td th:text="${gruppe.getMembers().size()}">Mitgliederanzahl</td>
|
<td th:text="${gruppe.getMembers().size()}">Mitgliederanzahl</td>
|
||||||
@ -59,6 +59,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user