Implement Discord appeal functionality, including database schema, API endpoints, front-end form, and Discord message handling.
This commit is contained in:
@@ -52,6 +52,7 @@ public class SecurityConfig {
|
||||
.requestMatchers("/api/login/getUsername").authenticated()
|
||||
.requestMatchers("/api/mail/**").authenticated()
|
||||
.requestMatchers("/api/site/vote").authenticated()
|
||||
.requestMatchers("/api/appeal").authenticated()
|
||||
.requestMatchers("/api/site/get-staff-playtime/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
|
||||
+10
-5
@@ -10,15 +10,14 @@ import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||
import com.alttd.altitudeweb.mappers.AppealDataMapper;
|
||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||
import com.alttd.altitudeweb.model.FormResponseDto;
|
||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||
import com.alttd.altitudeweb.model.*;
|
||||
import com.alttd.altitudeweb.services.forms.DiscordAppeal;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.mail.AppealMail;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -36,12 +35,18 @@ public class AppealController implements AppealsApi {
|
||||
|
||||
private final AppealDataMapper mapper;
|
||||
private final AppealMail appealMail;
|
||||
private final DiscordAppeal discordAppeal;
|
||||
private final com.alttd.altitudeweb.services.discord.AppealDiscord appealDiscord;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<BannedUserResponseDto> getBannedUser(Long discordId) throws Exception {
|
||||
return new ResponseEntity<>(discordAppeal.getBannedUser(discordId), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||
@Override
|
||||
public ResponseEntity<FormResponseDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
|
||||
return new ResponseEntity<>(discordAppeal.submitAppeal(discordAppealDto), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.alttd.altitudeweb.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.model.BannedUserDto;
|
||||
import com.alttd.webinterface.appeals.BannedUser;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class BannedUserToBannedUserDtoMapper {
|
||||
|
||||
public BannedUserDto map(BannedUser bannedUser) {
|
||||
return new BannedUserDto(bannedUser.userId(), bannedUser.reason(), bannedUser.name(), bannedUser.avatarUrl());
|
||||
}
|
||||
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.alttd.altitudeweb.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal;
|
||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class DiscordAppealDtoToDiscordAppealMapper {
|
||||
|
||||
public DiscordAppeal map(DiscordAppealDto discordAppealDto, UUID loggedInUserUuid, String discordUsername) {
|
||||
return new DiscordAppeal(
|
||||
UUID.randomUUID(),
|
||||
loggedInUserUuid,
|
||||
discordAppealDto.getDiscordId(),
|
||||
discordUsername,
|
||||
discordAppealDto.getAppeal(),
|
||||
Instant.now(),
|
||||
null,
|
||||
discordAppealDto.getEmail(),
|
||||
null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import com.alttd.altitudeweb.database.litebans.HistoryType;
|
||||
import com.alttd.altitudeweb.database.litebans.UserType;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.DiscordAppealMapper;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import com.alttd.webinterface.appeals.AppealSender;
|
||||
import com.alttd.webinterface.objects.MessageForEmbed;
|
||||
@@ -33,19 +35,67 @@ public class AppealDiscord {
|
||||
|
||||
private static final String OUTPUT_TYPE = "APPEAL";
|
||||
|
||||
public void sendAppealToDiscord(DiscordAppeal discordAppeal) {
|
||||
CompletableFuture<List<OutputChannel>> channelsFuture = getChannelListFuture();
|
||||
List<OutputChannel> channels = channelsFuture.join();
|
||||
if (channels.isEmpty()) {
|
||||
log.warn("Discord appeal: No Discord output channels found for type {}. Skipping Discord send.", OUTPUT_TYPE);
|
||||
return;
|
||||
}
|
||||
|
||||
String createdAt = formatInstant(discordAppeal.createdAt());
|
||||
List<DiscordSender.EmbedField> fields = new ArrayList<>();
|
||||
|
||||
// Group: User
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"User",
|
||||
"""
|
||||
Discord Username: %s
|
||||
Discord id: %s
|
||||
MC UUID: %s
|
||||
Submitted: %s
|
||||
""".formatted(
|
||||
safe(discordAppeal.username()),
|
||||
discordAppeal.discordId(),
|
||||
safe(String.valueOf(discordAppeal.uuid())),
|
||||
createdAt
|
||||
),
|
||||
false
|
||||
));
|
||||
|
||||
Optional<Long> optionalAssignedTo = assignAppeal();
|
||||
if (optionalAssignedTo.isPresent()) {
|
||||
Long assignedTo = optionalAssignedTo.get();
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"Assigned to",
|
||||
"Assigned to: <@" + assignedTo + ">",
|
||||
true
|
||||
));
|
||||
assignDiscordAppealTo(discordAppeal.id(), assignedTo);
|
||||
} else {
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"Assigned to",
|
||||
"Assigned to: None (failed to assign)",
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
String description = safe(discordAppeal.reason());
|
||||
|
||||
List<Long> channelIds = channels.stream()
|
||||
.map(OutputChannel::channel)
|
||||
.toList();
|
||||
|
||||
// colorRgb = null (use default), timestamp = appeal.createdAt if available
|
||||
Instant timestamp = discordAppeal.createdAt() != null ? discordAppeal.createdAt() : Instant.now();
|
||||
MessageForEmbed newAppealSubmitted = new MessageForEmbed(
|
||||
"New Discord Appeal Submitted", description, fields, null, timestamp, null);
|
||||
AppealSender.getInstance().sendAppeal(channelIds, newAppealSubmitted, optionalAssignedTo.orElse(0L));
|
||||
}
|
||||
|
||||
public void sendAppealToDiscord(Appeal appeal, HistoryRecord history) {
|
||||
// Fetch channels
|
||||
CompletableFuture<List<OutputChannel>> channelsFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DISCORD).runQuery(sql -> {
|
||||
try {
|
||||
List<OutputChannel> channels = sql.getMapper(OutputChannelMapper.class)
|
||||
.getChannelsWithOutputType(OUTPUT_TYPE);
|
||||
channelsFuture.complete(channels);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load output channels for {}", OUTPUT_TYPE, e);
|
||||
channelsFuture.complete(new ArrayList<>());
|
||||
}
|
||||
});
|
||||
CompletableFuture<List<OutputChannel>> channelsFuture = getChannelListFuture();
|
||||
|
||||
CompletableFuture<Integer> bansF = getCountAsync(HistoryType.BAN, appeal.uuid());
|
||||
CompletableFuture<Integer> mutesF = getCountAsync(HistoryType.MUTE, appeal.uuid());
|
||||
@@ -102,7 +152,7 @@ public class AppealDiscord {
|
||||
"Assigned to: <@" + assignedTo + ">",
|
||||
true
|
||||
));
|
||||
assignAppealTo(appeal.id(), assignedTo);
|
||||
assignMinecraftAppealTo(appeal.id(), assignedTo);
|
||||
} else {
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"Assigned to",
|
||||
@@ -124,7 +174,22 @@ public class AppealDiscord {
|
||||
AppealSender.getInstance().sendAppeal(channelIds, newAppealSubmitted, optionalAssignedTo.orElse(0L));
|
||||
}
|
||||
|
||||
private void assignAppealTo(UUID appealId, Long assignedTo) {
|
||||
private static CompletableFuture<List<OutputChannel>> getChannelListFuture() {
|
||||
CompletableFuture<List<OutputChannel>> channelsFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DISCORD).runQuery(sql -> {
|
||||
try {
|
||||
List<OutputChannel> channels = sql.getMapper(OutputChannelMapper.class)
|
||||
.getChannelsWithOutputType(OUTPUT_TYPE);
|
||||
channelsFuture.complete(channels);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load output channels for {}", OUTPUT_TYPE, e);
|
||||
channelsFuture.complete(new ArrayList<>());
|
||||
}
|
||||
});
|
||||
return channelsFuture;
|
||||
}
|
||||
|
||||
private void assignMinecraftAppealTo(UUID appealId, Long assignedTo) {
|
||||
Connection.getConnection(Databases.DEFAULT).runQuery(sql -> {
|
||||
try {
|
||||
sql.getMapper(AppealMapper.class).assignAppeal(appealId, assignedTo);
|
||||
@@ -134,6 +199,16 @@ public class AppealDiscord {
|
||||
});
|
||||
}
|
||||
|
||||
private void assignDiscordAppealTo(UUID appealId, Long assignedTo) {
|
||||
Connection.getConnection(Databases.DEFAULT).runQuery(sql -> {
|
||||
try {
|
||||
sql.getMapper(DiscordAppealMapper.class).assignDiscordAppeal(appealId, assignedTo);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to assign appeal to {}", assignedTo, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Integer> getCountAsync(HistoryType type, java.util.UUID uuid) {
|
||||
CompletableFuture<Integer> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sql -> {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.alttd.altitudeweb.services.forms;
|
||||
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.DiscordAppealMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||
import com.alttd.altitudeweb.mappers.BannedUserToBannedUserDtoMapper;
|
||||
import com.alttd.altitudeweb.mappers.DiscordAppealDtoToDiscordAppealMapper;
|
||||
import com.alttd.altitudeweb.model.BannedUserResponseDto;
|
||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||
import com.alttd.altitudeweb.model.FormResponseDto;
|
||||
import com.alttd.altitudeweb.services.discord.AppealDiscord;
|
||||
import com.alttd.altitudeweb.services.mail.AppealMail;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import com.alttd.webinterface.appeals.BannedUser;
|
||||
import com.alttd.webinterface.appeals.DiscordAppealDiscord;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DiscordAppeal {
|
||||
|
||||
private final BannedUserToBannedUserDtoMapper bannedUserToBannedUserDtoMapper;
|
||||
private final DiscordAppealDtoToDiscordAppealMapper discordAppealDtoToDiscordAppealMapper;
|
||||
private final AuthenticatedUuid authenticatedUuid;
|
||||
private final AppealDiscord appealDiscord;
|
||||
private final AppealMail appealMail;
|
||||
|
||||
public BannedUserResponseDto getBannedUser(Long discordId) {
|
||||
DiscordAppealDiscord discordAppeal = DiscordAppealDiscord.getInstance();
|
||||
Optional<BannedUser> join = discordAppeal.getBannedUser(discordId).join();
|
||||
if (join.isEmpty()) {
|
||||
return new BannedUserResponseDto(false);
|
||||
}
|
||||
BannedUserResponseDto bannedUserResponseDto = new BannedUserResponseDto(true);
|
||||
bannedUserResponseDto.setBannedUser(bannedUserToBannedUserDtoMapper.map(join.get()));
|
||||
return bannedUserResponseDto;
|
||||
}
|
||||
|
||||
public FormResponseDto submitAppeal(DiscordAppealDto discordAppealDto) {
|
||||
DiscordAppealDiscord discordAppealDiscord = DiscordAppealDiscord.getInstance();
|
||||
Optional<BannedUser> join = discordAppealDiscord.getBannedUser(discordAppealDto.getDiscordId()).join();
|
||||
if (join.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found");
|
||||
}
|
||||
BannedUser bannedUser = join.get();
|
||||
|
||||
Optional<UUID> uuid = authenticatedUuid.tryGetAuthenticatedUserUuid();
|
||||
if (uuid.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated");
|
||||
}
|
||||
|
||||
CompletableFuture<com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal> appealCompletableFuture = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading history by id");
|
||||
try {
|
||||
com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal discordAppealRecord = discordAppealDtoToDiscordAppealMapper
|
||||
.map(discordAppealDto, uuid.get(), bannedUser.name());
|
||||
sqlSession.getMapper(DiscordAppealMapper.class).createDiscordAppeal(discordAppealRecord);
|
||||
appealCompletableFuture.complete(discordAppealRecord);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load history count", e);
|
||||
appealCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal discordAppeal = appealCompletableFuture.join();
|
||||
|
||||
CompletableFuture<Optional<EmailVerification>> emailVerificationCompletableFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Retrieving mail by uuid and address");
|
||||
|
||||
EmailVerification verifiedMail = sqlSession.getMapper(EmailVerificationMapper.class)
|
||||
.findByUserAndEmail(discordAppeal.uuid(), discordAppeal.email().toLowerCase());
|
||||
emailVerificationCompletableFuture.complete(Optional.ofNullable(verifiedMail));
|
||||
});
|
||||
Optional<EmailVerification> optionalEmailVerification = emailVerificationCompletableFuture.join();
|
||||
|
||||
if (optionalEmailVerification.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid mail");
|
||||
}
|
||||
EmailVerification emailVerification = optionalEmailVerification.get();
|
||||
if (!emailVerification.verified()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Mail not verified");
|
||||
}
|
||||
|
||||
try {
|
||||
appealDiscord.sendAppealToDiscord(discordAppeal);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send appeal {} to Discord", discordAppeal.id(), e);
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to send appeal to Discord");
|
||||
}
|
||||
//TODO verify mail
|
||||
appealMail.sendAppealNotification(discordAppeal);
|
||||
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Marking appeal {} as sent", discordAppeal.id());
|
||||
sqlSession.getMapper(DiscordAppealMapper.class)
|
||||
.markDiscordAppealAsSent(discordAppeal.id());
|
||||
});
|
||||
return new FormResponseDto(
|
||||
discordAppeal.id().toString(),
|
||||
"Your appeal has been submitted. You will be notified when it has been reviewed.",
|
||||
true);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.alttd.altitudeweb.services.mail;
|
||||
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.DiscordAppeal;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -29,6 +30,20 @@ public class AppealMail {
|
||||
|
||||
private static final String APPEAL_EMAIL = "[email protected]";
|
||||
|
||||
/**
|
||||
* Sends an email notification about the appeal to both the user and the appeals team.
|
||||
*
|
||||
* @param appeal The appeal object containing all necessary information
|
||||
*/
|
||||
public void sendAppealNotification(DiscordAppeal appeal) {
|
||||
try {
|
||||
sendEmailToAppealsTeam(appeal);
|
||||
|
||||
log.info("Discord Appeal notification emails sent successfully for appeal ID: {}", appeal.id());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send discord appeal notification emails for appeal ID: {}", appeal.id(), e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sends an email notification about the appeal to both the user and the appeals team.
|
||||
*
|
||||
@@ -66,4 +81,29 @@ public class AppealMail {
|
||||
mailSender.send(message);
|
||||
}
|
||||
|
||||
private void sendEmailToAppealsTeam(DiscordAppeal appeal) throws MessagingException {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = getAppealMimeMessageHelper(appeal, message);
|
||||
|
||||
Context context = new Context();
|
||||
context.setVariable("appeal", appeal);
|
||||
context.setVariable("createdAt", appeal.createdAt()
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'")));
|
||||
String content = templateEngine.process("discord-appeal-email", context);
|
||||
|
||||
helper.setText(content, true);
|
||||
mailSender.send(message);
|
||||
}
|
||||
|
||||
private MimeMessageHelper getAppealMimeMessageHelper(DiscordAppeal appeal, MimeMessage message) throws MessagingException {
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
|
||||
helper.setFrom(fromEmail);
|
||||
helper.setTo(APPEAL_EMAIL);
|
||||
helper.setReplyTo(appeal.email());
|
||||
helper.setSubject("New Appeal Submitted - " + appeal.username());
|
||||
return helper;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
<div class="columnContainer">
|
||||
<div>
|
||||
<h2>Appeal:</h2>
|
||||
<p th:text="${appeal.reason}">Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
|
||||
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.</p>
|
||||
<p th:text="${appeal.reason}">appeal</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Discord Appeal Notification</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'minecraft-title';
|
||||
src: url('https://beta.alttd.com/public/fonts/minecraft-title.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-title.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-title.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-title.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'minecraft-text';
|
||||
src: url('https://beta.alttd.com/public/fonts/minecraft-text.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-text.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-text.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-text.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'opensans';
|
||||
src: url('https://beta.alttd.com/public/fonts/opensans.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'opensans-bold';
|
||||
src: url('https://beta.alttd.com/public/fonts/opensans-bold.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans-bold.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans-bold.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans-bold.woff') format('woff');
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'minecraft-title', sans-serif;
|
||||
}
|
||||
|
||||
.columnSection {
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.columnContainer {
|
||||
flex: 1 1 200px;
|
||||
min-width: 200px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
li, p {
|
||||
font-family: 'opensans', sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1150px) {
|
||||
.columnContainer, .columnSection {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 690px) {
|
||||
.columnContainer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<img id="header-img" src="https://beta.alttd.com/public/img/logos/logo.png" alt="The Altitude Minecraft Server" height="159"
|
||||
width="275">
|
||||
<h1 style="text-align: center;" th:text="'Appeal by ' + ${appeal.username}">Appeal by Username</h1>
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div>
|
||||
<h2>User information</h2>
|
||||
<ul>
|
||||
<li><strong>Username:</strong> <span th:text="${appeal.username}">username</span></li>
|
||||
<li><strong>UUID:</strong> <span th:text="${appeal.uuid}">uuid</span></li>
|
||||
<li><strong>Email:</strong> <span th:text="${appeal.email}">email</span></li>
|
||||
<li><strong>Submitted at:</strong> <span th:text="${createdAt}">date</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div>
|
||||
<h2>Appeal:</h2>
|
||||
<p th:text="${appeal.reason}">appeal</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user