mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-31 16:00:39 +08:00
finished admin log page
This commit is contained in:
parent
c4efbbc67a
commit
9949c3e6cc
@ -160,17 +160,17 @@ public class ApplicationController extends HangarController {
|
||||
@Secured("ROLE_USER")
|
||||
@GetMapping("/admin/log")
|
||||
public ModelAndView showLog(@RequestParam(required = false) Integer oPage,
|
||||
@RequestParam(required = false) Object userFilter,
|
||||
@RequestParam(required = false) Object projectFilter,
|
||||
@RequestParam(required = false) Object versionFilter,
|
||||
@RequestParam(required = false) Object pageFilter,
|
||||
@RequestParam(required = false) Object actionFilter,
|
||||
@RequestParam(required = false) Object subjectFilter) {
|
||||
@RequestParam(required = false) String userFilter,
|
||||
@RequestParam(required = false) String projectFilter,
|
||||
@RequestParam(required = false) String versionFilter,
|
||||
@RequestParam(required = false) String pageFilter,
|
||||
@RequestParam(required = false) String actionFilter,
|
||||
@RequestParam(required = false) String subjectFilter) {
|
||||
ModelAndView mv = new ModelAndView("users/admin/log");
|
||||
int pageSize = 50;
|
||||
int page = oPage != null ? oPage : 1;
|
||||
int offset = (page - 1) * pageSize;
|
||||
List<LoggedActionViewModel> log = userActionLogService.getLog(oPage, userFilter, projectFilter, versionFilter, pageFilter, actionFilter, subjectFilter);
|
||||
List<LoggedActionViewModel<?>> log = userActionLogService.getLog(oPage, userFilter, projectFilter, versionFilter, pageFilter, actionFilter, subjectFilter);
|
||||
mv.addObject("actions", log);
|
||||
mv.addObject("limit", pageSize);
|
||||
mv.addObject("offset", offset);
|
||||
|
@ -314,7 +314,7 @@ public class VersionsController extends HangarController {
|
||||
versionService.addUnstableTag(version.getId());
|
||||
}
|
||||
|
||||
userActionLogService.version(request, LoggedActionType.VERSION_UPLOADED.with(VersionContext.of(projectData.getProject().getId(), version.getId())), "published", "null");
|
||||
userActionLogService.version(request, LoggedActionType.VERSION_UPLOADED.with(VersionContext.of(projectData.getProject().getId(), version.getId())), "published", "");
|
||||
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("versions.show", author, slug, versionName));
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ public class LoggedActionType<C extends AbstractContext<C>> {
|
||||
this.value = value;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
loggedActionTypes.put(value.getValue(), this);
|
||||
}
|
||||
|
||||
private LoggedActionType(LoggedActionType<C> actionType, C actionContext) {
|
||||
|
@ -1,17 +1,27 @@
|
||||
package io.papermc.hangar.db.dao;
|
||||
|
||||
import io.papermc.hangar.db.mappers.LoggedActionViewModelMapper;
|
||||
import io.papermc.hangar.db.model.LoggedActionsOrganizationTable;
|
||||
import io.papermc.hangar.db.model.LoggedActionsPageTable;
|
||||
import io.papermc.hangar.db.model.LoggedActionsProjectTable;
|
||||
import io.papermc.hangar.db.model.LoggedActionsUserTable;
|
||||
import io.papermc.hangar.db.model.LoggedActionsVersionTable;
|
||||
|
||||
import io.papermc.hangar.model.viewhelpers.LoggedActionViewModel;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterColumnMapper;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindBean;
|
||||
import org.jdbi.v3.sqlobject.customizer.Define;
|
||||
import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings;
|
||||
import org.jdbi.v3.sqlobject.customizer.Timestamped;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RegisterBeanMapper(LoggedActionsOrganizationTable.class)
|
||||
@RegisterBeanMapper(LoggedActionsPageTable.class)
|
||||
@RegisterBeanMapper(LoggedActionsVersionTable.class)
|
||||
@ -39,4 +49,17 @@ public interface ActionsDao {
|
||||
@Timestamped
|
||||
@SqlUpdate("INSERT INTO logged_actions_organization (created_at, user_id, address, action, organization_id, new_state, old_state) VALUES (:now, :userId, :address, :action, :organizationId, :newState, :oldState)")
|
||||
void insertOrganizationLog(@BindBean LoggedActionsOrganizationTable loggedActionsOrganizationTable);
|
||||
|
||||
@UseStringTemplateEngine
|
||||
@RegisterRowMapper(LoggedActionViewModelMapper.class)
|
||||
@SqlQuery("SELECT * FROM v_logged_actions la " +
|
||||
" WHERE true " +
|
||||
"<if(userFilter)>AND la.user_name = '<userFilter>'<endif> " +
|
||||
"<if(projectFilter)>AND la.p_plugin_id = '<projectFilter>'<endif> " +
|
||||
"<if(versionFilter)>AND la.pv_version_string = '<versionFilter>'<endif> " +
|
||||
"<if(pageFilter)>AND la.pp_id = '<pageFilter>'<endif> " +
|
||||
"<if(actionFilter)>AND la.action = '<actionFilter>'::LOGGED_ACTION_TYPE<endif> " +
|
||||
"<if(subjectFilter)>AND la.s_name = '<subjectFilter>'<endif> " +
|
||||
"ORDER BY la.created_at DESC OFFSET <offset> LIMIT <pageSize>")
|
||||
List<LoggedActionViewModel<?>> getLog(@Define String userFilter, @Define String projectFilter, @Define String versionFilter, @Define String pageFilter, @Define String actionFilter, @Define String subjectFilter, @Define long offset, @Define long pageSize);
|
||||
}
|
||||
|
@ -0,0 +1,130 @@
|
||||
package io.papermc.hangar.db.mappers;
|
||||
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType.AbstractContext;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType.OrganizationContext;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType.ProjectContext;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType.ProjectPageContext;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType.UserContext;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType.VersionContext;
|
||||
import io.papermc.hangar.model.viewhelpers.LoggedActionViewModel;
|
||||
import io.papermc.hangar.model.viewhelpers.LoggedProject;
|
||||
import io.papermc.hangar.model.viewhelpers.LoggedProjectPage;
|
||||
import io.papermc.hangar.model.viewhelpers.LoggedProjectVersion;
|
||||
import io.papermc.hangar.model.viewhelpers.LoggedSubject;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public class LoggedActionViewModelMapper implements RowMapper<LoggedActionViewModel<?>> {
|
||||
@Override
|
||||
public LoggedActionViewModel<?> map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
long userId = rs.getLong("user_id");
|
||||
String userName = rs.getString("user_name");
|
||||
String address = rs.getString("address");
|
||||
LoggedActionType<? extends AbstractContext<?>> action = LoggedActionType.getLoggedActionType(rs.getString("action"));
|
||||
String newState = rs.getString("new_state");
|
||||
String oldState = rs.getString("old_state");
|
||||
OffsetDateTime createdAt = ctx.findColumnMapperFor(OffsetDateTime.class).get().map(rs, "created_at", ctx);
|
||||
|
||||
int contextType = rs.getInt("context_type");
|
||||
switch (contextType) {
|
||||
case 0:
|
||||
return new LoggedActionViewModel<>(userId,
|
||||
userName,
|
||||
address,
|
||||
(LoggedActionType<ProjectContext>) action,
|
||||
ProjectContext.of(rs.getLong("p_id")),
|
||||
newState,
|
||||
oldState,
|
||||
new LoggedProject(
|
||||
rs.getLong("p_id"),
|
||||
rs.getString("p_plugin_id"),
|
||||
rs.getString("p_slug"),
|
||||
rs.getString("p_owner_name")
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
createdAt);
|
||||
case 1:
|
||||
return new LoggedActionViewModel<>(
|
||||
userId,
|
||||
userName,
|
||||
address,
|
||||
(LoggedActionType<VersionContext>) action,
|
||||
VersionContext.of(rs.getLong("p_id"), rs.getLong("pv_id")),
|
||||
newState,
|
||||
oldState,
|
||||
null,
|
||||
new LoggedProjectVersion(
|
||||
rs.getLong("pv_id"),
|
||||
rs.getString("pv_version_string")
|
||||
),
|
||||
null,
|
||||
null,
|
||||
createdAt
|
||||
);
|
||||
case 2:
|
||||
return new LoggedActionViewModel<>(
|
||||
userId,
|
||||
userName,
|
||||
address,
|
||||
(LoggedActionType<ProjectPageContext>) action,
|
||||
ProjectPageContext.of(rs.getLong("p_id"), rs.getLong("pp_id")),
|
||||
newState,
|
||||
oldState,
|
||||
null,
|
||||
null,
|
||||
new LoggedProjectPage(
|
||||
rs.getLong("pp_id"),
|
||||
rs.getString("pp_name"),
|
||||
rs.getString("pp_slug")
|
||||
),
|
||||
null,
|
||||
createdAt
|
||||
);
|
||||
case 3:
|
||||
return new LoggedActionViewModel<>(
|
||||
userId,
|
||||
userName,
|
||||
address,
|
||||
(LoggedActionType<UserContext>) action,
|
||||
UserContext.of(rs.getLong("s_id")),
|
||||
newState,
|
||||
oldState,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new LoggedSubject(
|
||||
rs.getLong("s_id"),
|
||||
rs.getString("s_name")
|
||||
),
|
||||
createdAt
|
||||
);
|
||||
case 4:
|
||||
return new LoggedActionViewModel<>(
|
||||
userId,
|
||||
userName,
|
||||
address,
|
||||
(LoggedActionType<OrganizationContext>) action,
|
||||
OrganizationContext.of(rs.getLong("s_id")),
|
||||
newState,
|
||||
oldState,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new LoggedSubject(
|
||||
rs.getLong("s_id"),
|
||||
rs.getString("s_name")
|
||||
),
|
||||
createdAt
|
||||
);
|
||||
default:
|
||||
throw new IllegalArgumentException("Should be a value from 0 - 4");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +1,26 @@
|
||||
package io.papermc.hangar.model.viewhelpers;
|
||||
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType;
|
||||
import io.papermc.hangar.db.customtypes.LoggedActionType.AbstractContext;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public class LoggedActionViewModel {
|
||||
public class LoggedActionViewModel<C extends AbstractContext<C>> {
|
||||
|
||||
private long userId;
|
||||
private String userName;
|
||||
private String address;
|
||||
private LoggedActionType action;
|
||||
private LoggedActionType.AbstractContext actionContext;
|
||||
private String newState;
|
||||
private String oldState;
|
||||
private LoggedProject project;
|
||||
private LoggedProjectVersion version;
|
||||
private LoggedProjectPage page;
|
||||
private LoggedSubject subject;
|
||||
private OffsetDateTime createdAt;
|
||||
private final long userId;
|
||||
private final String userName;
|
||||
private final String address;
|
||||
private final LoggedActionType<C> action;
|
||||
private final C actionContext;
|
||||
private final String newState;
|
||||
private final String oldState;
|
||||
private final LoggedProject project;
|
||||
private final LoggedProjectVersion version;
|
||||
private final LoggedProjectPage page;
|
||||
private final LoggedSubject subject;
|
||||
private final OffsetDateTime createdAt;
|
||||
|
||||
public LoggedActionViewModel(long userId, String userName, String address, LoggedActionType action, LoggedActionType.AbstractContext actionContext, String newState, String oldState, LoggedProject project, LoggedProjectVersion version, LoggedProjectPage page, LoggedSubject subject, OffsetDateTime createdAt) {
|
||||
public LoggedActionViewModel(long userId, String userName, String address, LoggedActionType<C> action, C actionContext, String newState, String oldState, LoggedProject project, LoggedProjectVersion version, LoggedProjectPage page, LoggedSubject subject, OffsetDateTime createdAt) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
this.address = address;
|
||||
@ -46,11 +47,11 @@ public class LoggedActionViewModel {
|
||||
return address;
|
||||
}
|
||||
|
||||
public LoggedActionType getAction() {
|
||||
public LoggedActionType<C> getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public LoggedActionType.AbstractContext getActionContext() {
|
||||
public C getActionContext() {
|
||||
return actionContext;
|
||||
}
|
||||
|
||||
@ -81,4 +82,22 @@ public class LoggedActionViewModel {
|
||||
public OffsetDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LoggedActionViewModel{" +
|
||||
"userId=" + userId +
|
||||
", userName='" + userName + '\'' +
|
||||
", address='" + address + '\'' +
|
||||
", action=" + action +
|
||||
", actionContext=" + actionContext +
|
||||
", newState='" + newState + '\'' +
|
||||
", oldState='" + oldState + '\'' +
|
||||
", project=" + project +
|
||||
", version=" + version +
|
||||
", page=" + page +
|
||||
", subject=" + subject +
|
||||
", createdAt=" + createdAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,44 @@
|
||||
package io.papermc.hangar.model.viewhelpers;
|
||||
|
||||
import io.papermc.hangar.model.generated.Project;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class LoggedProject {
|
||||
|
||||
private Project project;
|
||||
private String pluginId;
|
||||
private String slug;
|
||||
private String owner;
|
||||
private final Long id;
|
||||
private final String pluginId;
|
||||
private final String slug;
|
||||
private final String owner;
|
||||
|
||||
public LoggedProject(@Nullable Project project, @Nullable String pluginId, @Nullable String slug, @Nullable String owner) {
|
||||
this.project = project;
|
||||
public LoggedProject(@Nullable Long id, @Nullable String pluginId, @Nullable String slug, @Nullable String owner) {
|
||||
this.id = id;
|
||||
this.pluginId = pluginId;
|
||||
this.slug = slug;
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Project getProject() {
|
||||
return project;
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPluginId() {
|
||||
return pluginId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSlug() {
|
||||
return slug;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LoggedProject{" +
|
||||
"id=" + id +
|
||||
", pluginId='" + pluginId + '\'' +
|
||||
", slug='" + slug + '\'' +
|
||||
", owner='" + owner + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
@ -4,28 +4,34 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class LoggedProjectPage {
|
||||
|
||||
private ProjectPage page;
|
||||
private String name;
|
||||
private String slug;
|
||||
private final Long id;
|
||||
private final String name;
|
||||
private final String slug;
|
||||
|
||||
public LoggedProjectPage(@Nullable ProjectPage page, @Nullable String name, @Nullable String slug) {
|
||||
this.page = page;
|
||||
public LoggedProjectPage(@Nullable Long id, @Nullable String name, @Nullable String slug) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ProjectPage getPage() {
|
||||
return page;
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSlug() {
|
||||
return slug;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LoggedProjectPage{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
", slug='" + slug + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,30 @@
|
||||
package io.papermc.hangar.model.viewhelpers;
|
||||
|
||||
import io.papermc.hangar.model.generated.Version;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class LoggedProjectVersion {
|
||||
|
||||
private Version version;
|
||||
private String versionString;
|
||||
private final Long id;
|
||||
private final String versionString;
|
||||
|
||||
public LoggedProjectVersion(@Nullable Version version, @Nullable String versionString) {
|
||||
this.version = version;
|
||||
public LoggedProjectVersion(@Nullable Long id, @Nullable String versionString) {
|
||||
this.id = id;
|
||||
this.versionString = versionString;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getVersionString() {
|
||||
return versionString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LoggedProjectVersion{" +
|
||||
"id=" + id +
|
||||
", versionString='" + versionString + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
@ -4,23 +4,29 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class LoggedSubject {
|
||||
|
||||
private Long id;
|
||||
private String username;
|
||||
private final Long id;
|
||||
private final String username;
|
||||
|
||||
public LoggedSubject(@Nullable Long id, @Nullable String username) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LoggedSubject{" +
|
||||
"id=" + id +
|
||||
", username='" + username + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -94,8 +94,15 @@ public class UserActionLogService {
|
||||
actionsDao.get().insertOrganizationLog(log);
|
||||
}
|
||||
|
||||
public List<LoggedActionViewModel> getLog(Integer oPage, Object userFilter, Object projectFilter, Object versionFilter, Object pageFilter, Object actionFilter, Object subjectFilter) {
|
||||
return new ArrayList<>(); //TODO See AppQueries.getLog(...)
|
||||
public List<LoggedActionViewModel<?>> getLog(Integer oPage, String userFilter, String projectFilter, String versionFilter, String pageFilter, String actionFilter, String subjectFilter) {
|
||||
long pageSize = 50L;
|
||||
long offset;
|
||||
if (oPage == null) {
|
||||
offset = 0;
|
||||
} else {
|
||||
offset = oPage * pageSize;
|
||||
}
|
||||
return actionsDao.get().getLog(userFilter, projectFilter, versionFilter, pageFilter, actionFilter, subjectFilter, offset, pageSize);
|
||||
}
|
||||
|
||||
private InetAddress getInetAddress(HttpServletRequest request) {
|
||||
|
@ -8,6 +8,11 @@
|
||||
</#assign>
|
||||
|
||||
<#assign message><@spring.message "admin.log.title" /></#assign>
|
||||
<#-- @ftlvariable name="canViewIP" type="java.lang.Boolean" -->
|
||||
<#-- @ftlvariable name="limit" type="java.lang.Integer" -->
|
||||
<#-- @ftlvariable name="offset" type="java.lang.Integer" -->
|
||||
<#-- @ftlvariable name="page" type="java.lang.Integer" -->
|
||||
<#-- @ftlvariable name="size" type="java.lang.Integer" -->
|
||||
<@base.base title="${message}" additionalScripts=scriptsVar>
|
||||
<div class="row">
|
||||
<div class="col-md-12 header-flags">
|
||||
@ -22,8 +27,7 @@
|
||||
<tr><td class="filter-project">Project</td> <td>${projectFilter!"-"}</td></tr>
|
||||
<tr><td class="filter-version">Version</td> <td>${versionFilter!"-"}</td></tr>
|
||||
<tr><td class="filter-page">Page</td><td>${pageFilter!"-"} </td></tr>
|
||||
<#assign LoggedActionType=@helper["io.papermc.hangar.db.customtypes.LoggedActionType"] />
|
||||
<tr><td class="filter-action">Action</td><td>${LoggedActionType.getLoggedActionType(actionFilter!"")!"-"}</td></tr>
|
||||
<tr><td class="filter-action">Action</td><td>${actionFilter!"-"}</td></tr>
|
||||
<tr><td class="filter-subject">Subject</td><td>${subjectFilter!"-"}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
@ -74,7 +78,7 @@
|
||||
<a href="${routes.getRouteUrl("users.showProjects", action.subject.username!"Unknown")}">${action.subject.username}</a>
|
||||
<small class="filter-subject">(<a href="${routes.getRouteUrl("showLog", page?string, userFilter, projectFilter, versionFilter, pageFilter, actionFilter, action.subject.username)}">${action.subject.username}</a>)</small>
|
||||
</td>
|
||||
<#elseif action.project.project??>
|
||||
<#elseif !action.project?? || !action.project.id??>
|
||||
<td>
|
||||
Resource deleted
|
||||
<#if action.actionContext.class.simpleName == "ProjectContext" || action.actionContext.class.simpleName == "ProjectPageContext">
|
||||
@ -112,8 +116,8 @@
|
||||
<textarea style="display: none" data-newstate="${offset + action?index}">${action.newState}</textarea>
|
||||
</td>
|
||||
<#else>
|
||||
<td>${action.oldState}</td>
|
||||
<td>${action.newState}</td>
|
||||
<td>${action.oldState?has_content?string(action.oldState, "<i>none</i>")}</td>
|
||||
<td>${action.newState?has_content?string(action.newState, "<i>none</i>")}</td>
|
||||
</#if>
|
||||
</tr>
|
||||
</#list>
|
||||
|
Loading…
x
Reference in New Issue
Block a user