diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java index dcab6c8..788ab4c 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java @@ -1,15 +1,92 @@ package com.alttd.altitudeweb.controllers.forms; import com.alttd.altitudeweb.api.ApplicationsApi; +import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid; +import com.alttd.altitudeweb.database.Databases; +import com.alttd.altitudeweb.database.web_db.forms.StaffApplication; +import com.alttd.altitudeweb.database.web_db.forms.StaffApplicationMapper; +import com.alttd.altitudeweb.database.web_db.mail.EmailVerification; +import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper; +import com.alttd.altitudeweb.mappers.StaffApplicationDataMapper; import com.alttd.altitudeweb.model.FormResponseDto; import com.alttd.altitudeweb.model.StaffApplicationDto; -import org.springframework.http.HttpStatusCode; +import com.alttd.altitudeweb.services.limits.RateLimit; +import com.alttd.altitudeweb.setup.Connection; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; -import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.bind.annotation.RestController; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@Slf4j +@RestController +@AllArgsConstructor +@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS) public class ApplicationController implements ApplicationsApi { + + private final StaffApplicationDataMapper staffApplicationDataMapper; + @Override public ResponseEntity submitStaffApplication(StaffApplicationDto staffApplicationDto) { - throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Staff applications are not yet supported"); + UUID userUuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + + StaffApplication application = staffApplicationDataMapper.map(userUuid, staffApplicationDto); + saveApplication(application); + + Optional optionalEmail = fetchEmailVerification(userUuid, application.email()); + boolean verified = optionalEmail.map(EmailVerification::verified).orElse(false); + + if (verified) { + markAsSent(application.id()); + } + + FormResponseDto response = buildResponse(application, verified); + return ResponseEntity.status(201).body(response); + } + + private void saveApplication(StaffApplication application) { + CompletableFuture saveFuture = new CompletableFuture<>(); + Connection.getConnection(Databases.DEFAULT) + .runQuery(sqlSession -> { + try { + sqlSession.getMapper(StaffApplicationMapper.class).insert(application); + saveFuture.complete(null); + } catch (Exception e) { + log.error("Failed to insert staff application", e); + saveFuture.completeExceptionally(e); + } + }); + saveFuture.join(); + } + + private Optional fetchEmailVerification(UUID userUuid, String email) { + CompletableFuture> emailVerificationFuture = new CompletableFuture<>(); + Connection.getConnection(Databases.DEFAULT) + .runQuery(sqlSession -> { + EmailVerification verifiedMail = sqlSession.getMapper(EmailVerificationMapper.class) + .findByUserAndEmail(userUuid, email); + emailVerificationFuture.complete(Optional.ofNullable(verifiedMail)); + }); + return emailVerificationFuture.join(); + } + + private void markAsSent(UUID applicationId) { + Connection.getConnection(Databases.DEFAULT) + .runQuery(sqlSession -> sqlSession.getMapper(StaffApplicationMapper.class).markAsSent(applicationId)); + } + + private FormResponseDto buildResponse(StaffApplication application, boolean verified) { + String message = verified + ? "Your staff application has been submitted. You will be notified when it has been reviewed." + : "Application created. Please verify your email to complete submission."; + return new FormResponseDto( + application.id().toString(), + message, + true + ); } } diff --git a/backend/src/main/java/com/alttd/altitudeweb/mappers/StaffApplicationDataMapper.java b/backend/src/main/java/com/alttd/altitudeweb/mappers/StaffApplicationDataMapper.java new file mode 100644 index 0000000..3be9d8a --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/mappers/StaffApplicationDataMapper.java @@ -0,0 +1,53 @@ +package com.alttd.altitudeweb.mappers; + +import com.alttd.altitudeweb.database.web_db.forms.StaffApplication; +import com.alttd.altitudeweb.model.StaffApplicationDto; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +public class StaffApplicationDataMapper { + + /** + * Maps the incoming DTO and the authenticated user's UUID to a StaffApplication entity. + * Normalizes and prepares fields as needed (lowercase email, join availableDays, timestamps, ids). + */ + public StaffApplication map(UUID userUuid, StaffApplicationDto dto) { + String email = dto.getEmail() == null ? null : dto.getEmail().toLowerCase(); + String availableDaysJoined = joinList(dto.getAvailableDays()); + + return new StaffApplication( + UUID.randomUUID(), + userUuid, + email, + dto.getAge(), + dto.getDiscordUsername(), + Boolean.TRUE.equals(dto.getMeetsRequirements()), + dto.getPronouns(), + dto.getJoinDate(), + dto.getWeeklyPlaytime(), + availableDaysJoined, + dto.getAvailableTimes(), + dto.getPreviousExperience(), + dto.getPluginExperience(), + dto.getModeratorExpectations(), + dto.getAdditionalInfo(), + Instant.now(), + null, + null + ); + } + + private String joinList(List list) { + if (list == null) return null; + // Avoid NPEs and trim entries + return list.stream() + .filter(s -> s != null && !s.isBlank()) + .map(String::trim) + .collect(Collectors.joining(",")); + } +} diff --git a/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/StaffApplication.java b/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/StaffApplication.java new file mode 100644 index 0000000..bec6067 --- /dev/null +++ b/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/StaffApplication.java @@ -0,0 +1,28 @@ +package com.alttd.altitudeweb.database.web_db.forms; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +public record StaffApplication( + UUID id, + UUID uuid, + String email, + Integer age, + String discordUsername, + Boolean meetsRequirements, + String pronouns, + LocalDate joinDate, + Integer weeklyPlaytime, + String availableDays, + String availableTimes, + String previousExperience, + String pluginExperience, + String moderatorExpectations, + String additionalInfo, + Instant createdAt, + Instant sendAt, + Long assignedTo +) { +} diff --git a/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/StaffApplicationMapper.java b/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/StaffApplicationMapper.java new file mode 100644 index 0000000..c7c8d96 --- /dev/null +++ b/database/src/main/java/com/alttd/altitudeweb/database/web_db/forms/StaffApplicationMapper.java @@ -0,0 +1,29 @@ +package com.alttd.altitudeweb.database.web_db.forms; + +import org.apache.ibatis.annotations.*; + +import java.util.UUID; + +public interface StaffApplicationMapper { + + @Insert(""" + INSERT INTO staff_applications ( + id, uuid, email, age, discord_username, meets_requirements, pronouns, join_date, + weekly_playtime, available_days, available_times, previous_experience, plugin_experience, + moderator_expectations, additional_info, created_at, send_at, assigned_to + ) VALUES ( + #{id}, #{uuid}, #{email}, #{age}, #{discordUsername}, #{meetsRequirements}, #{pronouns}, #{joinDate}, + #{weeklyPlaytime}, + #{availableDays}, + #{availableTimes}, #{previousExperience}, #{pluginExperience}, + #{moderatorExpectations}, #{additionalInfo}, #{createdAt}, #{sendAt}, #{assignedTo} + ) + """) + void insert(StaffApplication application); + + @Update(""" + UPDATE staff_applications SET send_at = NOW() + WHERE id = #{id} + """) + void markAsSent(@Param("id") UUID id); +} diff --git a/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java b/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java index 44328d2..44670c6 100644 --- a/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java +++ b/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java @@ -5,6 +5,7 @@ import com.alttd.altitudeweb.database.web_db.KeyPairMapper; import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper; import com.alttd.altitudeweb.database.web_db.SettingsMapper; import com.alttd.altitudeweb.database.web_db.forms.AppealMapper; +import com.alttd.altitudeweb.database.web_db.forms.StaffApplicationMapper; import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.session.SqlSession; @@ -23,6 +24,7 @@ public class InitializeWebDb { configuration.addMapper(KeyPairMapper.class); configuration.addMapper(PrivilegedUserMapper.class); configuration.addMapper(AppealMapper.class); + configuration.addMapper(StaffApplicationMapper.class); configuration.addMapper(EmailVerificationMapper.class); }).join() .runQuery(sqlSession -> { @@ -31,6 +33,7 @@ public class InitializeWebDb { createPrivilegedUsersTable(sqlSession); createPrivilegesTable(sqlSession); createAppealTable(sqlSession); + createStaffApplicationsTable(sqlSession); createUserEmailsTable(sqlSession); }); log.debug("Initialized WebDb"); @@ -126,6 +129,37 @@ public class InitializeWebDb { } } + private static void createStaffApplicationsTable(@NotNull SqlSession sqlSession) { + String query = """ + CREATE TABLE IF NOT EXISTS staff_applications ( + id UUID NOT NULL DEFAULT (UUID()) PRIMARY KEY, + uuid UUID NOT NULL, + email VARCHAR(320) NOT NULL, + age INT NOT NULL, + discord_username VARCHAR(32) NOT NULL, + meets_requirements BOOLEAN NOT NULL, + pronouns VARCHAR(32) NULL, + join_date DATE NOT NULL, + weekly_playtime INT NOT NULL, + available_days TEXT NOT NULL, + available_times TEXT NOT NULL, + previous_experience TEXT NOT NULL, + plugin_experience TEXT NOT NULL, + moderator_expectations TEXT NOT NULL, + additional_info TEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + send_at TIMESTAMP NULL, + assigned_to BIGINT UNSIGNED NULL, + FOREIGN KEY (uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE + ); + """; + try (Statement statement = sqlSession.getConnection().createStatement()) { + statement.execute(query); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + private static void createAppealTable(@NotNull SqlSession sqlSession) { String query = """ CREATE TABLE IF NOT EXISTS appeals (