Compare commits
No commits in common. "c4c17b3adce76cdbe08d18266a33bab599dcf293" and "643545a18a6e929bd30f51b5cd30b4f016004dcb" have entirely different histories.
c4c17b3adc
...
643545a18a
|
|
@ -35,7 +35,6 @@ dependencies {
|
|||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
implementation("org.springframework.boot:spring-boot-configuration-processor")
|
||||
implementation("org.springframework.boot:spring-boot-starter-hateoas")
|
||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||
|
||||
//AOP
|
||||
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||
|
|
|
|||
|
|
@ -1,29 +1,21 @@
|
|||
package com.alttd.altitudeweb.controllers.application;
|
||||
|
||||
import com.alttd.altitudeweb.api.AppealsApi;
|
||||
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.model.AppealResponseDto;
|
||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
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 {
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||
@Override
|
||||
public ResponseEntity<MinecraftAppealDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
|
||||
}
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
|
||||
@Override
|
||||
public ResponseEntity<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Minecraft appeals are not yet supported");
|
||||
|
|
|
|||
|
|
@ -153,24 +153,6 @@ public class HistoryApiController implements HistoryApi {
|
|||
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
|
||||
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
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,151 +1,22 @@
|
|||
package com.alttd.altitudeweb.controllers.login;
|
||||
|
||||
import com.alttd.altitudeweb.api.LoginApi;
|
||||
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
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 com.alttd.altitudeweb.model.AddLoginDto;
|
||||
import com.alttd.altitudeweb.model.LoginDataDto;
|
||||
import com.alttd.altitudeweb.model.LoginResultDto;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
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 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 {
|
||||
|
||||
private final KeyPairService keyPairService;
|
||||
private final String loginSecret =System.getenv("LOGIN_SECRET") ;
|
||||
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
|
||||
public ResponseEntity<String> requestLogin(String authorization, String uuid) {
|
||||
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);
|
||||
public ResponseEntity<Void> addLogin(AddLoginDto addLoginDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Adding login is not yet supported");
|
||||
}
|
||||
|
||||
@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);
|
||||
public ResponseEntity<LoginResultDto> login(LoginDataDto loginDataDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Logging in is not yet supported");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,4 @@ database.host=${DB_HOST:localhost}
|
|||
database.user=${DB_USER:root}
|
||||
database.password=${DB_PASSWORD:root}
|
||||
cors.allowed-origins=${CORS:https://alttd.com}
|
||||
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||
logging.level.com.alttd.altitudeweb=INFO
|
||||
|
|
|
|||
|
|
@ -80,10 +80,6 @@ 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,
|
||||
@NotNull UUID uuid, int page) {
|
||||
int offset = page * PAGE_SIZE;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ public interface TeamMemberMapper {
|
|||
FROM luckperms_user_permissions AS permissions
|
||||
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||
WHERE permission = #{groupPermission}
|
||||
AND world = 'global'
|
||||
""")
|
||||
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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,7 +87,6 @@ public class Connection {
|
|||
log.debug("Loaded default database settings {}", databaseSettings);
|
||||
Connection connection = new Connection(databaseSettings, addMappers);
|
||||
log.debug("Created default database connection {}", connection);
|
||||
connections.put(Databases.DEFAULT, connection);
|
||||
return CompletableFuture.completedFuture(connection);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.alttd.altitudeweb.setup;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
|
|
@ -13,16 +12,12 @@ import java.sql.Statement;
|
|||
public class InitializeWebDb {
|
||||
|
||||
protected static void init() {
|
||||
log.info("Initializing WebDb");
|
||||
log.info("Initializing LiteBans");
|
||||
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
||||
configuration.addMapper(SettingsMapper.class);
|
||||
configuration.addMapper(KeyPairMapper.class);
|
||||
}).join()
|
||||
.runQuery(SqlSession -> {
|
||||
createSettingsTable(SqlSession);
|
||||
createKeyTable(SqlSession);
|
||||
});
|
||||
log.debug("Initialized WebDb");
|
||||
.runQuery(InitializeWebDb::createSettingsTable);
|
||||
log.debug("Initialized LuckPerms");
|
||||
}
|
||||
|
||||
private static void createSettingsTable(SqlSession sqlSession) {
|
||||
|
|
@ -45,20 +40,4 @@ 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 {FormsComponent} from '../forms.component';
|
||||
import {FormControl, FormGroup, Validators} from '@angular/forms';
|
||||
import {AppealsService, MinecraftAppeal} from '../../../api';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {AppealsService} from '../../../api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-appeal',
|
||||
|
|
@ -13,18 +13,22 @@ import {AppealsService, MinecraftAppeal} from '../../../api';
|
|||
})
|
||||
export class AppealComponent implements OnInit {
|
||||
|
||||
public form: FormGroup<Appeal>;
|
||||
public form: FormGroup | undefined;
|
||||
|
||||
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)]})
|
||||
});
|
||||
constructor(private fb: FormBuilder, private appealApi: AppealsService) {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
|
@ -33,7 +37,8 @@ export class AppealComponent implements OnInit {
|
|||
return
|
||||
}
|
||||
if (this.form.valid) {
|
||||
this.sendForm()
|
||||
console.log('Form submitted:', this.form.value);
|
||||
// Process form submission here
|
||||
} else {
|
||||
// Mark all fields as touched to trigger validation display
|
||||
Object.keys(this.form.controls).forEach(field => {
|
||||
|
|
@ -47,23 +52,12 @@ export class AppealComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
private sendForm() {
|
||||
const rawValue = this.form.getRawValue();
|
||||
const appeal: MinecraftAppeal = {
|
||||
appeal: rawValue.appeal,
|
||||
email: rawValue.email,
|
||||
punishmentId: parseInt(rawValue.punishmentId),
|
||||
username: rawValue.username,
|
||||
uuid: ''//TODO
|
||||
}
|
||||
this.appealApi.submitMinecraftAppeal(appeal).subscribe()
|
||||
private sendForm(validForm: FormGroup) {
|
||||
// const appeal: MinecraftAppeal = {
|
||||
//
|
||||
// }
|
||||
// this.appealApi.submitMinecraftAppeal()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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"
|
||||
height="160" width="160" style="width: 160px;">
|
||||
<h2>{{ member.name }}</h2>
|
||||
<p>Head Mod</p>
|
||||
<p>Admin</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ paths:
|
|||
$ref: './schemas/bans/bans.yml#/getTotalResultsForUserSearch'
|
||||
/history/single/{type}/{id}:
|
||||
$ref: './schemas/bans/bans.yml#/getHistoryById'
|
||||
/history/all/{uuid}:
|
||||
$ref: './schemas/bans/bans.yml#/getAllHistoryForUUID'
|
||||
/history/total:
|
||||
$ref: './schemas/bans/bans.yml#/getTotalPunishments'
|
||||
/appeal/update-mail:
|
||||
|
|
@ -38,7 +36,7 @@ paths:
|
|||
$ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal'
|
||||
/appeal/discord-appeal:
|
||||
$ref: './schemas/forms/appeal/appeal.yml#/DiscordAppeal'
|
||||
/login/requestNewUserLogin/{uuid}:
|
||||
$ref: './schemas/login/login.yml#/RequestNewUserLogin'
|
||||
/login/userLogin/{code}:
|
||||
/login/addUserLogin:
|
||||
$ref: './schemas/login/login.yml#/AddUserLogin'
|
||||
/login/userLogin:
|
||||
$ref: './schemas/login/login.yml#/UserLogin'
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ getHistoryForUuid:
|
|||
parameters:
|
||||
- $ref: '#/components/parameters/UserType'
|
||||
- $ref: '#/components/parameters/HistoryType'
|
||||
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||
- $ref: '#/components/parameters/Uuid'
|
||||
- $ref: '#/components/parameters/Page'
|
||||
responses:
|
||||
'200':
|
||||
|
|
@ -158,7 +158,7 @@ getTotalResultsForUuidSearch:
|
|||
parameters:
|
||||
- $ref: '#/components/parameters/UserType'
|
||||
- $ref: '#/components/parameters/HistoryType'
|
||||
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||
- $ref: '#/components/parameters/Uuid'
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
|
|
@ -195,22 +195,6 @@ getHistoryById:
|
|||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
parameters:
|
||||
HistoryType:
|
||||
|
|
@ -228,6 +212,13 @@ components:
|
|||
schema:
|
||||
type: string
|
||||
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:
|
||||
name: userType
|
||||
in: path
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
components:
|
||||
parameters:
|
||||
Uuid:
|
||||
name: uuid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The uuid of the desired user
|
||||
|
|
@ -1,61 +1,51 @@
|
|||
UserLogin:
|
||||
get:
|
||||
post:
|
||||
tags:
|
||||
- login
|
||||
summary: Log in to the site
|
||||
description: Log in to the site through a code from the server
|
||||
operationId: login
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Code'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginData'
|
||||
responses:
|
||||
'200':
|
||||
description: Logged in
|
||||
content:
|
||||
application/text:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: A JWT token for this user
|
||||
$ref: '#/components/schemas/LoginResult'
|
||||
'401':
|
||||
description: Login failed - Invalid credentials
|
||||
content:
|
||||
application/text:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/text:
|
||||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
RequestNewUserLogin:
|
||||
get:
|
||||
tags:
|
||||
- login
|
||||
summary: Request a login
|
||||
description: Request a code, that can be used to log in
|
||||
operationId: requestLogin
|
||||
parameters:
|
||||
- name: Authorization
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Secret
|
||||
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||
responses:
|
||||
'200':
|
||||
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'
|
||||
AddUserLogin:
|
||||
post:
|
||||
tags:
|
||||
- login
|
||||
summary: Add a login
|
||||
description: Add a code, user combination that can be used to log in
|
||||
operationId: addLogin
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AddLogin'
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
|
|
@ -63,14 +53,6 @@ RequestNewUserLogin:
|
|||
schema:
|
||||
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||
components:
|
||||
parameters:
|
||||
Code:
|
||||
name: code
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The code to log in with
|
||||
schemas:
|
||||
LoginData:
|
||||
type: object
|
||||
|
|
@ -80,11 +62,35 @@ components:
|
|||
loginCode:
|
||||
type: string
|
||||
description: The code to log in
|
||||
AddLogin:
|
||||
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:
|
||||
type: object
|
||||
required:
|
||||
- loginCode
|
||||
- uuid
|
||||
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:
|
||||
type: string
|
||||
format: uuid
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user