Compare commits
6 Commits
643545a18a
...
c4c17b3adc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4c17b3adc | ||
|
|
cf758bfe60 | ||
|
|
8c7ec0a237 | ||
|
|
80462218a7 | ||
|
|
26b5f86983 | ||
|
|
ba6cf6d938 |
|
|
@ -35,6 +35,7 @@ dependencies {
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
implementation("org.springframework.boot:spring-boot-configuration-processor")
|
implementation("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-hateoas")
|
implementation("org.springframework.boot:spring-boot-starter-hateoas")
|
||||||
|
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||||
|
|
||||||
//AOP
|
//AOP
|
||||||
implementation("org.aspectj:aspectjrt:1.9.19")
|
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,29 @@
|
||||||
package com.alttd.altitudeweb.controllers.application;
|
package com.alttd.altitudeweb.controllers.application;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.AppealsApi;
|
import com.alttd.altitudeweb.api.AppealsApi;
|
||||||
|
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
||||||
import com.alttd.altitudeweb.model.AppealResponseDto;
|
import com.alttd.altitudeweb.model.AppealResponseDto;
|
||||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||||
import com.alttd.altitudeweb.model.UpdateMailDto;
|
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||||
import org.springframework.http.HttpStatusCode;
|
import org.springframework.http.HttpStatusCode;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||||
public class AppealController implements AppealsApi {
|
public class AppealController implements AppealsApi {
|
||||||
|
|
||||||
|
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<MinecraftAppealDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
public ResponseEntity<MinecraftAppealDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
||||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
|
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
public ResponseEntity<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Minecraft appeals are not yet supported");
|
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Minecraft appeals are not yet supported");
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,24 @@ public class HistoryApiController implements HistoryApi {
|
||||||
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
|
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<PunishmentHistoryListDto> getAllHistoryForUUID(String uuid) {
|
||||||
|
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||||
|
CompletableFuture<List<HistoryRecord>> historyRecordsCompletableFuture = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading all history for uuid {}", uuid);
|
||||||
|
try {
|
||||||
|
List<HistoryRecord> punishments = sqlSession.getMapper(UUIDHistoryMapper.class)
|
||||||
|
.getAllHistoryForUUID(UUID.fromString(uuid));
|
||||||
|
historyRecordsCompletableFuture.complete(punishments);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load all history for uuid {}", uuid, e);
|
||||||
|
historyRecordsCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mapPunishmentHistory(punishmentHistoryList, historyRecordsCompletableFuture);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
|
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
|
||||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package com.alttd.altitudeweb.controllers.login;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.KeyPairEntity;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||||
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.security.*;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class KeyPairService {
|
||||||
|
|
||||||
|
private KeyPair cachedKeyPair = null;
|
||||||
|
private static final String RSA_ALGORITHM = "RSA";
|
||||||
|
private static final int RSA_KEY_SIZE = 2048;
|
||||||
|
|
||||||
|
public KeyPair getJwtSigningKeyPair() {
|
||||||
|
if (cachedKeyPair != null) {
|
||||||
|
return cachedKeyPair;
|
||||||
|
}
|
||||||
|
KeyPair keyPair = getOrCreateKeyPair();
|
||||||
|
if (keyPair != null) {
|
||||||
|
cachedKeyPair = keyPair;
|
||||||
|
return cachedKeyPair;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Failed to generate or load key pair");
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPair getOrCreateKeyPair() {
|
||||||
|
CompletableFuture<KeyPairEntity> keyPairFuture = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading key pair");
|
||||||
|
try {
|
||||||
|
KeyPairEntity entity = sqlSession.getMapper(KeyPairMapper.class).getKeyPair();
|
||||||
|
keyPairFuture.complete(entity);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to key pair", e);
|
||||||
|
keyPairFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
KeyPairEntity keyPairEntity = keyPairFuture.join();
|
||||||
|
if (keyPairEntity != null) {
|
||||||
|
try {
|
||||||
|
byte[] privateKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPrivateKey());
|
||||||
|
byte[] publicKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPublicKey());
|
||||||
|
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
|
||||||
|
PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
|
||||||
|
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
|
||||||
|
|
||||||
|
return new KeyPair(publicKey, privateKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load key pair from database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyPair keyPair = generateKeyPair();
|
||||||
|
|
||||||
|
try {
|
||||||
|
KeyPairEntity entity = new KeyPairEntity();
|
||||||
|
entity.setPrivateKey(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
|
||||||
|
entity.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
|
||||||
|
entity.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Saving key pair");
|
||||||
|
try {
|
||||||
|
sqlSession.getMapper(KeyPairMapper.class).save(entity);
|
||||||
|
log.info("Generated and saved new key pair");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to key pair", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to save key pair to database", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyPair generateKeyPair() {
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
|
||||||
|
keyPairGenerator.initialize(RSA_KEY_SIZE);
|
||||||
|
return keyPairGenerator.generateKeyPair();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("Error generating key pair", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,151 @@
|
||||||
package com.alttd.altitudeweb.controllers.login;
|
package com.alttd.altitudeweb.controllers.login;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.LoginApi;
|
import com.alttd.altitudeweb.api.LoginApi;
|
||||||
import com.alttd.altitudeweb.model.AddLoginDto;
|
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
||||||
import com.alttd.altitudeweb.model.LoginDataDto;
|
import com.nimbusds.jose.jwk.JWK;
|
||||||
import com.alttd.altitudeweb.model.LoginResultDto;
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.RSAKey;
|
||||||
|
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||||
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||||
|
import com.nimbusds.jose.proc.SecurityContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatusCode;
|
import org.springframework.http.HttpStatusCode;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||||
|
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
public class LoginController implements LoginApi {
|
public class LoginController implements LoginApi {
|
||||||
|
|
||||||
@Override
|
private final KeyPairService keyPairService;
|
||||||
public ResponseEntity<Void> addLogin(AddLoginDto addLoginDto) {
|
private final String loginSecret =System.getenv("LOGIN_SECRET") ;
|
||||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Adding login is not yet supported");
|
private record CacheEntry(UUID uuid, Instant expiry) {}
|
||||||
|
|
||||||
|
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 300000) // 5 minutes in milliseconds
|
||||||
|
private void clearExpiredCacheEntries() {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
int initialCacheSize = cache.size();
|
||||||
|
cache.entrySet().removeIf(entry -> entry.getValue().expiry().isBefore(now));
|
||||||
|
log.info("Cleared {} expired cache entries", initialCacheSize - cache.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RateLimit(limit = 100, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "addLogin")
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<LoginResultDto> login(LoginDataDto loginDataDto) {
|
public ResponseEntity<String> requestLogin(String authorization, String uuid) {
|
||||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Logging in is not yet supported");
|
UUID uuidFromString;
|
||||||
|
try {
|
||||||
|
uuidFromString = UUID.fromString(uuid);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return new ResponseEntity<>(HttpStatusCode.valueOf(400));
|
||||||
|
}
|
||||||
|
if (authorization == null || !authorization.startsWith("SECRET ")) {
|
||||||
|
return new ResponseEntity<>(HttpStatusCode.valueOf(403));
|
||||||
|
}
|
||||||
|
|
||||||
|
String secret = authorization.substring("SECRET ".length());
|
||||||
|
if (!isValidSecret(secret)) {
|
||||||
|
throw new ResponseStatusException(HttpStatusCode.valueOf(401), "Invalid secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<String> key = cache.entrySet().stream()
|
||||||
|
.filter(entry -> entry.getValue().uuid.equals(uuidFromString))
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (key.isPresent()) {
|
||||||
|
return ResponseEntity.ok(key.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
String loginCode = generateLoginCode(uuidFromString);
|
||||||
|
return ResponseEntity.ok(loginCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<String> login(String code) {
|
||||||
|
if ( code == null) {
|
||||||
|
return new ResponseEntity<>(HttpStatusCode.valueOf(400));
|
||||||
|
}
|
||||||
|
CacheEntry cacheEntry = cache.get(code);
|
||||||
|
if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) {
|
||||||
|
return new ResponseEntity<>(HttpStatusCode.valueOf(403));
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok().body(getJWTToken(cacheEntry.uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateLoginCode(UUID uuid) {
|
||||||
|
String characters = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||||
|
StringBuilder loginCode = new StringBuilder();
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
int index = (int) (Math.random() * characters.length());
|
||||||
|
loginCode.append(characters.charAt(index));
|
||||||
|
}
|
||||||
|
CacheEntry cacheEntry = new CacheEntry(uuid,
|
||||||
|
Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
|
||||||
|
cache.put(loginCode.toString(), cacheEntry);
|
||||||
|
return loginCode.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidSecret(String secret) {
|
||||||
|
if (loginSecret == null) {
|
||||||
|
log.warn("No login secret set, skipping secret validation");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (loginSecret.length() < 16) {
|
||||||
|
log.warn("Login secret is too short, skipping secret validation");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!loginSecret.equals(secret)) {
|
||||||
|
log.info("Received invalid secret {}", secret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getJWTToken(UUID uuid) {
|
||||||
|
JwtEncoder jwtEncoder = jwtEncoder();
|
||||||
|
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
|
||||||
|
|
||||||
|
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||||
|
.issuer("altitudeweb")
|
||||||
|
.issuedAt(now)
|
||||||
|
.expiresAt(expiryTime)
|
||||||
|
.subject("user")
|
||||||
|
.claim("uuid", uuid.toString())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JwtEncoder jwtEncoder() {
|
||||||
|
KeyPair keyPair = keyPairService.getJwtSigningKeyPair();
|
||||||
|
JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
|
||||||
|
.privateKey((RSAPrivateKey) keyPair.getPrivate())
|
||||||
|
.build();
|
||||||
|
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
|
||||||
|
return new NimbusJwtEncoder(jwkSource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,5 @@ database.host=${DB_HOST:localhost}
|
||||||
database.user=${DB_USER:root}
|
database.user=${DB_USER:root}
|
||||||
database.password=${DB_PASSWORD:root}
|
database.password=${DB_PASSWORD:root}
|
||||||
cors.allowed-origins=${CORS:https://alttd.com}
|
cors.allowed-origins=${CORS:https://alttd.com}
|
||||||
|
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||||
logging.level.com.alttd.altitudeweb=INFO
|
logging.level.com.alttd.altitudeweb=INFO
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,10 @@ public interface UUIDHistoryMapper {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default List<HistoryRecord> getAllHistoryForUUID(@NotNull UUID uuid) {
|
||||||
|
return getRecentAllHistory(uuid.toString(), "uuid", 100, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private List<HistoryRecord> getRecent(@NotNull String tableName, @NotNull UserType userType,
|
private List<HistoryRecord> getRecent(@NotNull String tableName, @NotNull UserType userType,
|
||||||
@NotNull UUID uuid, int page) {
|
@NotNull UUID uuid, int page) {
|
||||||
int offset = page * PAGE_SIZE;
|
int offset = page * PAGE_SIZE;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ public interface TeamMemberMapper {
|
||||||
FROM luckperms_user_permissions AS permissions
|
FROM luckperms_user_permissions AS permissions
|
||||||
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||||
WHERE permission = #{groupPermission}
|
WHERE permission = #{groupPermission}
|
||||||
|
AND world = 'global'
|
||||||
""")
|
""")
|
||||||
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class KeyPairEntity {
|
||||||
|
private int id;
|
||||||
|
private String privateKey;
|
||||||
|
private String publicKey;
|
||||||
|
private Instant createdAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.*;
|
||||||
|
|
||||||
|
public interface KeyPairMapper {
|
||||||
|
|
||||||
|
@Select("SELECT * FROM key_pair ORDER BY id DESC LIMIT 1")
|
||||||
|
KeyPairEntity getKeyPair();
|
||||||
|
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO key_pair (id, private_key, public_key, created_at)
|
||||||
|
VALUES (#{id}, #{privateKey}, #{publicKey}, #{createdAt})
|
||||||
|
""")
|
||||||
|
void save(KeyPairEntity keyPair);
|
||||||
|
}
|
||||||
|
|
@ -87,6 +87,7 @@ public class Connection {
|
||||||
log.debug("Loaded default database settings {}", databaseSettings);
|
log.debug("Loaded default database settings {}", databaseSettings);
|
||||||
Connection connection = new Connection(databaseSettings, addMappers);
|
Connection connection = new Connection(databaseSettings, addMappers);
|
||||||
log.debug("Created default database connection {}", connection);
|
log.debug("Created default database connection {}", connection);
|
||||||
|
connections.put(Databases.DEFAULT, connection);
|
||||||
return CompletableFuture.completedFuture(connection);
|
return CompletableFuture.completedFuture(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.alttd.altitudeweb.setup;
|
package com.alttd.altitudeweb.setup;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.ibatis.session.SqlSession;
|
import org.apache.ibatis.session.SqlSession;
|
||||||
|
|
@ -12,12 +13,16 @@ import java.sql.Statement;
|
||||||
public class InitializeWebDb {
|
public class InitializeWebDb {
|
||||||
|
|
||||||
protected static void init() {
|
protected static void init() {
|
||||||
log.info("Initializing LiteBans");
|
log.info("Initializing WebDb");
|
||||||
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
||||||
configuration.addMapper(SettingsMapper.class);
|
configuration.addMapper(SettingsMapper.class);
|
||||||
|
configuration.addMapper(KeyPairMapper.class);
|
||||||
}).join()
|
}).join()
|
||||||
.runQuery(InitializeWebDb::createSettingsTable);
|
.runQuery(SqlSession -> {
|
||||||
log.debug("Initialized LuckPerms");
|
createSettingsTable(SqlSession);
|
||||||
|
createKeyTable(SqlSession);
|
||||||
|
});
|
||||||
|
log.debug("Initialized WebDb");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createSettingsTable(SqlSession sqlSession) {
|
private static void createSettingsTable(SqlSession sqlSession) {
|
||||||
|
|
@ -40,4 +45,20 @@ public class InitializeWebDb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void createKeyTable(SqlSession sqlSession) {
|
||||||
|
String query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS key_pair (
|
||||||
|
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
private_key TEXT NOT NULL,
|
||||||
|
public_key TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
|
statement.execute(query);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {FormsComponent} from '../forms.component';
|
import {FormsComponent} from '../forms.component';
|
||||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
import {FormControl, FormGroup, Validators} from '@angular/forms';
|
||||||
import {AppealsService} from '../../../api';
|
import {AppealsService, MinecraftAppeal} from '../../../api';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-appeal',
|
selector: 'app-appeal',
|
||||||
|
|
@ -13,22 +13,18 @@ import {AppealsService} from '../../../api';
|
||||||
})
|
})
|
||||||
export class AppealComponent implements OnInit {
|
export class AppealComponent implements OnInit {
|
||||||
|
|
||||||
public form: FormGroup | undefined;
|
public form: FormGroup<Appeal>;
|
||||||
|
|
||||||
constructor(private fb: FormBuilder, private appealApi: AppealsService) {
|
constructor(private appealApi: AppealsService) {
|
||||||
|
this.form = new FormGroup({
|
||||||
|
username: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
|
||||||
|
punishmentId: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
|
||||||
|
email: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}),
|
||||||
|
appeal: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.minLength(10)]})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.initForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
private initForm() {
|
|
||||||
this.form = this.fb.group({
|
|
||||||
name: ['', [Validators.required]],
|
|
||||||
punishmentId: ['', [Validators.required]],
|
|
||||||
email: ['', [Validators.required, Validators.email]],
|
|
||||||
message: ['', [Validators.required, Validators.minLength(10)]]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSubmit() {
|
public onSubmit() {
|
||||||
|
|
@ -37,8 +33,7 @@ export class AppealComponent implements OnInit {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.form.valid) {
|
if (this.form.valid) {
|
||||||
console.log('Form submitted:', this.form.value);
|
this.sendForm()
|
||||||
// Process form submission here
|
|
||||||
} else {
|
} else {
|
||||||
// Mark all fields as touched to trigger validation display
|
// Mark all fields as touched to trigger validation display
|
||||||
Object.keys(this.form.controls).forEach(field => {
|
Object.keys(this.form.controls).forEach(field => {
|
||||||
|
|
@ -52,12 +47,23 @@ export class AppealComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendForm(validForm: FormGroup) {
|
private sendForm() {
|
||||||
// const appeal: MinecraftAppeal = {
|
const rawValue = this.form.getRawValue();
|
||||||
//
|
const appeal: MinecraftAppeal = {
|
||||||
// }
|
appeal: rawValue.appeal,
|
||||||
// this.appealApi.submitMinecraftAppeal()
|
email: rawValue.email,
|
||||||
|
punishmentId: parseInt(rawValue.punishmentId),
|
||||||
|
username: rawValue.username,
|
||||||
|
uuid: ''//TODO
|
||||||
|
}
|
||||||
|
this.appealApi.submitMinecraftAppeal(appeal).subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Appeal {
|
||||||
|
username: FormControl<string>;
|
||||||
|
punishmentId: FormControl<string>;
|
||||||
|
email: FormControl<string>;
|
||||||
|
appeal: FormControl<string>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||||
height="160" width="160" style="width: 160px;">
|
height="160" width="160" style="width: 160px;">
|
||||||
<h2>{{ member.name }}</h2>
|
<h2>{{ member.name }}</h2>
|
||||||
<p>Admin</p>
|
<p>Head Mod</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ paths:
|
||||||
$ref: './schemas/bans/bans.yml#/getTotalResultsForUserSearch'
|
$ref: './schemas/bans/bans.yml#/getTotalResultsForUserSearch'
|
||||||
/history/single/{type}/{id}:
|
/history/single/{type}/{id}:
|
||||||
$ref: './schemas/bans/bans.yml#/getHistoryById'
|
$ref: './schemas/bans/bans.yml#/getHistoryById'
|
||||||
|
/history/all/{uuid}:
|
||||||
|
$ref: './schemas/bans/bans.yml#/getAllHistoryForUUID'
|
||||||
/history/total:
|
/history/total:
|
||||||
$ref: './schemas/bans/bans.yml#/getTotalPunishments'
|
$ref: './schemas/bans/bans.yml#/getTotalPunishments'
|
||||||
/appeal/update-mail:
|
/appeal/update-mail:
|
||||||
|
|
@ -36,7 +38,7 @@ paths:
|
||||||
$ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal'
|
$ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal'
|
||||||
/appeal/discord-appeal:
|
/appeal/discord-appeal:
|
||||||
$ref: './schemas/forms/appeal/appeal.yml#/DiscordAppeal'
|
$ref: './schemas/forms/appeal/appeal.yml#/DiscordAppeal'
|
||||||
/login/addUserLogin:
|
/login/requestNewUserLogin/{uuid}:
|
||||||
$ref: './schemas/login/login.yml#/AddUserLogin'
|
$ref: './schemas/login/login.yml#/RequestNewUserLogin'
|
||||||
/login/userLogin:
|
/login/userLogin/{code}:
|
||||||
$ref: './schemas/login/login.yml#/UserLogin'
|
$ref: './schemas/login/login.yml#/UserLogin'
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ getHistoryForUuid:
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/UserType'
|
- $ref: '#/components/parameters/UserType'
|
||||||
- $ref: '#/components/parameters/HistoryType'
|
- $ref: '#/components/parameters/HistoryType'
|
||||||
- $ref: '#/components/parameters/Uuid'
|
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||||
- $ref: '#/components/parameters/Page'
|
- $ref: '#/components/parameters/Page'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -158,7 +158,7 @@ getTotalResultsForUuidSearch:
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/UserType'
|
- $ref: '#/components/parameters/UserType'
|
||||||
- $ref: '#/components/parameters/HistoryType'
|
- $ref: '#/components/parameters/HistoryType'
|
||||||
- $ref: '#/components/parameters/Uuid'
|
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful operation
|
description: Successful operation
|
||||||
|
|
@ -195,6 +195,22 @@ getHistoryById:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
getAllHistoryForUUID:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- history
|
||||||
|
summary: Gets all history for specified UUID
|
||||||
|
description: Retrieves all history for specified UUID
|
||||||
|
operationId: getAllHistoryForUUID
|
||||||
|
parameters:
|
||||||
|
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PunishmentHistoryList'
|
||||||
components:
|
components:
|
||||||
parameters:
|
parameters:
|
||||||
HistoryType:
|
HistoryType:
|
||||||
|
|
@ -212,13 +228,6 @@ components:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
description: The (partial) username to search for
|
description: The (partial) username to search for
|
||||||
Uuid:
|
|
||||||
name: uuid
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The uuid of the desired user
|
|
||||||
UserType:
|
UserType:
|
||||||
name: userType
|
name: userType
|
||||||
in: path
|
in: path
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
components:
|
||||||
|
parameters:
|
||||||
|
Uuid:
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The uuid of the desired user
|
||||||
|
|
@ -1,51 +1,61 @@
|
||||||
UserLogin:
|
UserLogin:
|
||||||
post:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- login
|
- login
|
||||||
summary: Log in to the site
|
summary: Log in to the site
|
||||||
description: Log in to the site through a code from the server
|
description: Log in to the site through a code from the server
|
||||||
operationId: login
|
operationId: login
|
||||||
requestBody:
|
parameters:
|
||||||
required: true
|
- $ref: '#/components/parameters/Code'
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/LoginData'
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Logged in
|
description: Logged in
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/text:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/LoginResult'
|
type: string
|
||||||
|
description: A JWT token for this user
|
||||||
'401':
|
'401':
|
||||||
description: Login failed - Invalid credentials
|
description: Login failed - Invalid credentials
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/text:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
default:
|
default:
|
||||||
description: Unexpected error
|
description: Unexpected error
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/text:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
AddUserLogin:
|
RequestNewUserLogin:
|
||||||
post:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- login
|
- login
|
||||||
summary: Add a login
|
summary: Request a login
|
||||||
description: Add a code, user combination that can be used to log in
|
description: Request a code, that can be used to log in
|
||||||
operationId: addLogin
|
operationId: requestLogin
|
||||||
requestBody:
|
parameters:
|
||||||
required: true
|
- name: Authorization
|
||||||
content:
|
in: header
|
||||||
application/json:
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AddLogin'
|
type: string
|
||||||
|
description: Secret
|
||||||
|
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
description: Success
|
||||||
|
content:
|
||||||
|
application/text:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: code to log in with
|
||||||
|
'401':
|
||||||
|
description: Login failed - Invalid secret
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
default:
|
default:
|
||||||
description: Unexpected error
|
description: Unexpected error
|
||||||
content:
|
content:
|
||||||
|
|
@ -53,6 +63,14 @@ AddUserLogin:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
components:
|
components:
|
||||||
|
parameters:
|
||||||
|
Code:
|
||||||
|
name: code
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The code to log in with
|
||||||
schemas:
|
schemas:
|
||||||
LoginData:
|
LoginData:
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -62,35 +80,11 @@ components:
|
||||||
loginCode:
|
loginCode:
|
||||||
type: string
|
type: string
|
||||||
description: The code to log in
|
description: The code to log in
|
||||||
LoginResult:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- uuid
|
|
||||||
- userName
|
|
||||||
- auth
|
|
||||||
properties:
|
|
||||||
uuid:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: UUID of logged in user
|
|
||||||
userName:
|
|
||||||
type: string
|
|
||||||
description: Name of the logged in user
|
|
||||||
auth:
|
|
||||||
type: string
|
|
||||||
description: Token to use along side requests
|
|
||||||
AddLogin:
|
AddLogin:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- loginCode
|
|
||||||
- uuid
|
- uuid
|
||||||
properties:
|
properties:
|
||||||
auth:
|
|
||||||
type: string
|
|
||||||
description: Token to verify the sender is allowed to add logins
|
|
||||||
loginCode:
|
|
||||||
type: string
|
|
||||||
description: The code that can be logged in with
|
|
||||||
uuid:
|
uuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user