Add email verification functionality, including backend support, email handling, and user interface integration.
This commit is contained in:
parent
da17cf9696
commit
641083732d
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(project(":open_api"))
|
||||
implementation(project(":database"))
|
||||
implementation(project(":frontend"))
|
||||
implementation(project(":discord"))
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
implementation("com.mysql:mysql-connector-j:8.0.32")
|
||||
implementation("org.mybatis:mybatis:3.5.13")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.nimbusds.jose.proc.SecurityContext;
|
|||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
|
|
@ -39,15 +40,17 @@ public class SecurityConfig {
|
|||
return http
|
||||
.authorizeHttpRequests(
|
||||
auth -> auth
|
||||
.requestMatchers("/api/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||
.requestMatchers("/api/login/userLogin").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.requestMatchers("/api/form/**").authenticated()
|
||||
.requestMatchers("/api/login/getUsername").authenticated()
|
||||
.requestMatchers("/api/mail/**").authenticated()
|
||||
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/login/userLogin/**").permitAll()
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.anonymous(AbstractHttpConfigurer::disable)
|
||||
.oauth2ResourceServer(
|
||||
oauth2 -> oauth2
|
||||
.jwt(Customizer.withDefaults())
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.alttd.altitudeweb.database.litebans.HistoryType;
|
|||
import com.alttd.altitudeweb.database.litebans.IdHistoryMapper;
|
||||
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.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||
import com.alttd.altitudeweb.mappers.AppealDataMapper;
|
||||
import com.alttd.altitudeweb.model.AppealResponseDto;
|
||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||
|
|
@ -22,6 +24,7 @@ import org.springframework.http.ResponseEntity;
|
|||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -64,14 +67,39 @@ public class AppealController implements AppealsApi {
|
|||
throw new ResponseStatusException(HttpStatusCode.valueOf(404), "History not found");
|
||||
}
|
||||
|
||||
appealMail.sendAppealNotification(appeal, history);
|
||||
CompletableFuture<Optional<EmailVerification>> emailVerificationCompletableFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Retrieving mail by uuid and address");
|
||||
|
||||
AppealResponseDto appealResponseDto = new AppealResponseDto(
|
||||
appeal.id().toString(),
|
||||
"Your appeal has been submitted. You will be notified when it has been reviewed.",
|
||||
false);
|
||||
EmailVerification verifiedMail = sqlSession.getMapper(EmailVerificationMapper.class)
|
||||
.findByUserAndEmail(appeal.uuid(), appeal.email());
|
||||
emailVerificationCompletableFuture.complete(Optional.ofNullable(verifiedMail));
|
||||
});
|
||||
Optional<EmailVerification> optionalEmailVerification = emailVerificationCompletableFuture.join();
|
||||
|
||||
return ResponseEntity.ok().body(appealResponseDto);
|
||||
if (optionalEmailVerification.isEmpty()) {
|
||||
return ResponseEntity.ok().body(new AppealResponseDto(
|
||||
appeal.id().toString(),
|
||||
"Your appeal has been saved and a verification mail has been send, please verify your email " +
|
||||
"address by clicking the link in your email. Once it is verified we will review your appeal.",
|
||||
false));
|
||||
}
|
||||
EmailVerification emailVerification = optionalEmailVerification.get();
|
||||
if (!emailVerification.verified()) {
|
||||
return ResponseEntity.ok().body(new AppealResponseDto(
|
||||
appeal.id().toString(),
|
||||
"Your appeal has been saved and a verification mail has been resend, please verify your email " +
|
||||
"address by clicking the link in your email. Once it is verified we will review your appeal.",
|
||||
false
|
||||
));
|
||||
} else {
|
||||
AppealResponseDto appealResponseDto = new AppealResponseDto(
|
||||
appeal.id().toString(),
|
||||
"Your appeal has been submitted. You will be notified when it has been reviewed.",
|
||||
true);
|
||||
return ResponseEntity.ok().body(appealResponseDto);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package com.alttd.altitudeweb.controllers.forms;
|
||||
|
||||
import com.alttd.altitudeweb.api.MailApi;
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.model.MailResponseDto;
|
||||
import com.alttd.altitudeweb.model.SubmitEmailDto;
|
||||
import com.alttd.altitudeweb.model.VerifyCodeDto;
|
||||
import com.alttd.altitudeweb.model.EmailEntryDto;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.mail.MailVerificationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RateLimit(limit = 60, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||
public class MailController implements MailApi {
|
||||
|
||||
private final MailVerificationService mailVerificationService;
|
||||
|
||||
@Override
|
||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailSubmit")
|
||||
public ResponseEntity<MailResponseDto> submitEmailForVerification(SubmitEmailDto submitEmailDto) {
|
||||
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||
EmailVerification saved = mailVerificationService.submitEmail(uuid, submitEmailDto.getEmail());
|
||||
MailResponseDto response = new MailResponseDto()
|
||||
.email(saved.email())
|
||||
.message("Verification email sent")
|
||||
.verified(false);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RateLimit(limit = 20, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailVerify")
|
||||
public ResponseEntity<MailResponseDto> verifyEmailCode(VerifyCodeDto verifyCodeDto) {
|
||||
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||
Optional<EmailVerification> optionalEmailVerification = mailVerificationService.verifyCode(uuid, verifyCodeDto.getCode());
|
||||
if (optionalEmailVerification.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid verification code");
|
||||
}
|
||||
EmailVerification emailVerification = optionalEmailVerification.get();
|
||||
MailResponseDto response = new MailResponseDto()
|
||||
.email(emailVerification.email())
|
||||
.message("Email verified successfully")
|
||||
.verified(true);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailResend")
|
||||
public ResponseEntity<MailResponseDto> resendVerificationEmail(SubmitEmailDto submitEmailDto) {
|
||||
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||
EmailVerification updated = mailVerificationService.resend(uuid, submitEmailDto.getEmail());
|
||||
if (updated == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Email not found for user");
|
||||
}
|
||||
MailResponseDto response = new MailResponseDto()
|
||||
.email(updated.email())
|
||||
.message("Verification email resent")
|
||||
.verified(false);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RateLimit(limit = 10, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailDelete")
|
||||
public ResponseEntity<Void> deleteEmail(SubmitEmailDto submitEmailDto) {
|
||||
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||
boolean deleted = mailVerificationService.delete(uuid, submitEmailDto.getEmail());
|
||||
if (!deleted) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Email not found for user");
|
||||
}
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<List<EmailEntryDto>> getUserEmails() {
|
||||
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||
List<EmailVerification> emails = mailVerificationService.listAll(uuid);
|
||||
List<EmailEntryDto> result = emails.stream()
|
||||
.map(ev -> new EmailEntryDto().email(ev.email()).verified(ev.verified()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
|
|
@ -134,20 +134,32 @@ public class LoginController implements LoginApi {
|
|||
return username.join();
|
||||
}
|
||||
|
||||
@Value("${UNSECURED:#{false}}")
|
||||
private boolean unsecured;
|
||||
|
||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
||||
@Override
|
||||
public ResponseEntity<String> login(String code) {
|
||||
if (unsecured) {
|
||||
log.warn("Unsecured login is enabled, skipping login validation!");
|
||||
} else {
|
||||
log.info("Received login request with code {}", code);
|
||||
}
|
||||
if (code == null) {
|
||||
log.warn("Received null login code");
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
CacheEntry cacheEntry = cache.get(code);
|
||||
if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) {
|
||||
if (!unsecured && (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now()))) {
|
||||
log.warn("Received invalid login code {}", code);
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
if (unsecured && cacheEntry == null) {
|
||||
cacheEntry = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
|
||||
}
|
||||
|
||||
String token = generateToken(cacheEntry.uuid);
|
||||
log.debug("Generated token for user {} with token {}", cacheEntry.uuid, token);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
|||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
|
|
@ -21,9 +22,11 @@ public class AppealDataMapper {
|
|||
return new Appeal(
|
||||
UUID.randomUUID(),
|
||||
minecraftAppealDto.getUuid(),
|
||||
minecraftAppealDto.getPunishmentType().toString(),
|
||||
minecraftAppealDto.getPunishmentId(),
|
||||
minecraftAppealDto.getUsername(),
|
||||
minecraftAppealDto.getAppeal(),
|
||||
null,
|
||||
Instant.now(),
|
||||
null,
|
||||
minecraftAppealDto.getEmail(),
|
||||
null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
package com.alttd.altitudeweb.services.mail;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MailVerificationService {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String fromEmail;
|
||||
|
||||
public java.util.List<EmailVerification> listAll(UUID userUuid) {
|
||||
java.util.concurrent.CompletableFuture<java.util.List<EmailVerification>> future = new java.util.concurrent.CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
future.complete(mapper.findAllByUser(userUuid));
|
||||
});
|
||||
return future.join();
|
||||
}
|
||||
|
||||
public EmailVerification submitEmail(UUID userUuid, String email) {
|
||||
String code = generateCode();
|
||||
Instant now = Instant.now();
|
||||
|
||||
CompletableFuture<EmailVerification> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
EmailVerification existing = mapper.findByUserAndEmail(userUuid, email);
|
||||
EmailVerification toPersist;
|
||||
if (existing == null) {
|
||||
toPersist = new EmailVerification(UUID.randomUUID(), userUuid, email, code, false, now, null, now);
|
||||
mapper.insert(toPersist);
|
||||
} else {
|
||||
mapper.updateCodeAndLastSent(existing.id(), code, now);
|
||||
toPersist = new EmailVerification(existing.id(), userUuid, email, code, false, existing.createdAt(), null, now);
|
||||
}
|
||||
future.complete(toPersist);
|
||||
});
|
||||
EmailVerification saved = future.join();
|
||||
sendVerificationEmail(saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Optional<EmailVerification> verifyCode(UUID userUuid, String code) {
|
||||
CompletableFuture<Optional<EmailVerification>> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
EmailVerification found = mapper.findByUserAndCode(userUuid, code);
|
||||
if (found == null) {
|
||||
future.complete(Optional.empty());
|
||||
return;
|
||||
}
|
||||
mapper.markVerified(found.id(), Instant.now());
|
||||
future.complete(Optional.of(found));
|
||||
});
|
||||
return future.join();
|
||||
}
|
||||
|
||||
public EmailVerification resend(UUID userUuid, String email) {
|
||||
String code = generateCode();
|
||||
Instant now = Instant.now();
|
||||
CompletableFuture<EmailVerification> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
EmailVerification existing = mapper.findByUserAndEmail(userUuid, email);
|
||||
if (existing != null) {
|
||||
mapper.updateCodeAndLastSent(existing.id(), code, now);
|
||||
future.complete(new EmailVerification(existing.id(), userUuid, email, code, false, existing.createdAt(), null, now));
|
||||
} else {
|
||||
future.complete(null);
|
||||
}
|
||||
});
|
||||
EmailVerification updated = future.join();
|
||||
if (updated != null) {
|
||||
sendVerificationEmail(updated);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
public boolean delete(UUID userUuid, String email) {
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
EmailVerification existing = mapper.findByUserAndEmail(userUuid, email);
|
||||
if (existing != null) {
|
||||
mapper.deleteByUserAndEmail(userUuid, email);
|
||||
future.complete(true);
|
||||
} else {
|
||||
future.complete(false);
|
||||
}
|
||||
});
|
||||
return future.join();
|
||||
}
|
||||
|
||||
private void sendVerificationEmail(EmailVerification emailVerification) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
helper.setFrom(fromEmail);
|
||||
helper.setTo(emailVerification.email());
|
||||
helper.setSubject("Your verification code");
|
||||
helper.setText("Your verification code is: " + emailVerification.verificationCode(), false);
|
||||
mailSender.send(message);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Failed to send verification email to {}", emailVerification.email(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateCode() {
|
||||
// 6-digit numeric code
|
||||
Random random = new Random();
|
||||
int num = 100000 + random.nextInt(899999);
|
||||
return String.valueOf(num);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,4 @@
|
|||
spring.application.name=AltitudeWeb
|
||||
database.name=${DB_NAME:web_db}
|
||||
database.port=${DB_PORT:3306}
|
||||
database.host=${DB_HOST:localhost}
|
||||
database.user=${DB_USER:root}
|
||||
database.password=${DB_PASSWORD:root}
|
||||
cors.allowed-origins=${CORS:http://localhost:4200}
|
||||
my-server.address=${SERVER_ADDRESS:http://localhost}
|
||||
cors.allowed-origins=${CORS:http://localhost:4200,http://localhost:8080}
|
||||
my-server.address=${SERVER_ADDRESS:http://localhost:8080}
|
||||
logging.level.com.alttd.altitudeweb=DEBUG
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import lombok.Getter;
|
|||
public enum Databases {
|
||||
DEFAULT("web_db"),
|
||||
LUCK_PERMS("luckperms"),
|
||||
LITE_BANS("litebans");
|
||||
LITE_BANS("litebans"),
|
||||
DISCORD("discordLink");
|
||||
|
||||
private final String internalName;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
package com.alttd.altitudeweb.database.discord;
|
||||
|
||||
public record OutputChannel(long guild, String outputType, long channel, String channelType) {
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.alttd.altitudeweb.database.discord;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface OutputChannelMapper {
|
||||
@Select("""
|
||||
SELECT guild, output_type, channel, channel_type
|
||||
FROM output_channels
|
||||
WHERE output_type = #{outputType}
|
||||
""")
|
||||
List<OutputChannel> getChannelsWithOutputType(@Param("outputType") String outputType);
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import java.util.UUID;
|
|||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PrivilegedUser {
|
||||
private int id;
|
||||
private Integer id;
|
||||
private UUID uuid;
|
||||
private List<String> permissions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.alttd.altitudeweb.database.web_db.mail;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record EmailVerification(
|
||||
UUID id,
|
||||
UUID userUuid,
|
||||
String email,
|
||||
String verificationCode,
|
||||
boolean verified,
|
||||
Instant createdAt,
|
||||
Instant verifiedAt,
|
||||
Instant lastSentAt
|
||||
) {
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.alttd.altitudeweb.database.web_db.mail;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface EmailVerificationMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO user_emails (id, user_uuid, email, verification_code, verified, created_at, last_sent_at)
|
||||
VALUES (#{id}, #{userUuid}, #{email}, #{verificationCode}, #{verified}, #{createdAt}, #{lastSentAt})
|
||||
""")
|
||||
void insert(EmailVerification emailVerification);
|
||||
|
||||
@Select("""
|
||||
SELECT id, user_uuid AS userUuid, email, verification_code AS verificationCode, verified,
|
||||
created_at AS createdAt, verified_at AS verifiedAt, last_sent_at AS lastSentAt
|
||||
FROM user_emails
|
||||
WHERE user_uuid = #{userUuid} AND email = #{email}
|
||||
""")
|
||||
EmailVerification findByUserAndEmail(@Param("userUuid") UUID userUuid, @Param("email") String email);
|
||||
|
||||
@Select("""
|
||||
SELECT id, user_uuid AS userUuid, email, verification_code AS verificationCode, verified,
|
||||
created_at AS createdAt, verified_at AS verifiedAt, last_sent_at AS lastSentAt
|
||||
FROM user_emails
|
||||
WHERE user_uuid = #{userUuid} AND verification_code = #{code}
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""")
|
||||
EmailVerification findByUserAndCode(@Param("userUuid") UUID userUuid, @Param("code") String code);
|
||||
|
||||
@Select("""
|
||||
SELECT id, user_uuid AS userUuid, email, verification_code AS verificationCode, verified,
|
||||
created_at AS createdAt, verified_at AS verifiedAt, last_sent_at AS lastSentAt
|
||||
FROM user_emails
|
||||
WHERE user_uuid = #{userUuid}
|
||||
ORDER BY created_at ASC
|
||||
""")
|
||||
java.util.List<EmailVerification> findAllByUser(@Param("userUuid") UUID userUuid);
|
||||
|
||||
@Update("""
|
||||
UPDATE user_emails SET verified = 1, verified_at = #{verifiedAt}
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
void markVerified(@Param("id") UUID id, @Param("verifiedAt") Instant verifiedAt);
|
||||
|
||||
@Update("""
|
||||
UPDATE user_emails SET verification_code = #{code}, last_sent_at = #{lastSentAt}, verified = 0, verified_at = NULL
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
void updateCodeAndLastSent(@Param("id") UUID id, @Param("code") String code, @Param("lastSentAt") Instant lastSentAt);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM user_emails WHERE user_uuid = #{userUuid} AND email = #{email}
|
||||
""")
|
||||
void deleteByUserAndEmail(@Param("userUuid") UUID userUuid, @Param("email") String email);
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ public class Connection {
|
|||
InitializeWebDb.init();
|
||||
InitializeLiteBans.init();
|
||||
InitializeLuckPerms.init();
|
||||
InitializeDiscord.init();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.alttd.altitudeweb.setup;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.discord.OutputChannelMapper;
|
||||
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class InitializeDiscord {
|
||||
|
||||
protected static void init() {
|
||||
log.info("Initializing Discord");
|
||||
Connection.getConnection(Databases.DISCORD, (configuration) -> {
|
||||
configuration.addMapper(OutputChannelMapper.class);
|
||||
}).join();
|
||||
log.debug("Initialized Discord");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ public class InitializeWebDb {
|
|||
configuration.addMapper(KeyPairMapper.class);
|
||||
configuration.addMapper(PrivilegedUserMapper.class);
|
||||
configuration.addMapper(AppealMapper.class);
|
||||
configuration.addMapper(com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper.class);
|
||||
}).join()
|
||||
.runQuery(sqlSession -> {
|
||||
createSettingsTable(sqlSession);
|
||||
|
|
@ -29,6 +30,7 @@ public class InitializeWebDb {
|
|||
createPrivilegedUsersTable(sqlSession);
|
||||
createPrivilegesTable(sqlSession);
|
||||
createAppealTable(sqlSession);
|
||||
createUserEmailsTable(sqlSession);
|
||||
});
|
||||
log.debug("Initialized WebDb");
|
||||
}
|
||||
|
|
@ -102,6 +104,27 @@ public class InitializeWebDb {
|
|||
}
|
||||
}
|
||||
|
||||
private static void createUserEmailsTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS user_emails (
|
||||
id UUID NOT NULL DEFAULT (UUID()) PRIMARY KEY,
|
||||
user_uuid UUID NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
verification_code VARCHAR(16) NOT NULL,
|
||||
verified BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
verified_at TIMESTAMP NULL,
|
||||
last_sent_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (user_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 (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import {AfterViewInit, Component, ElementRef, OnInit, Renderer2, signal} from '@angular/core';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
computed,
|
||||
ElementRef,
|
||||
inject,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {AppealsService, HistoryService, MinecraftAppeal, PunishmentHistory} from '@api';
|
||||
import {AppealsService, EmailEntry, HistoryService, MailService, MinecraftAppeal, PunishmentHistory} from '@api';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {NgOptimizedImage} from '@angular/common';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
|
|
@ -28,19 +38,23 @@ import {HistoryFormatService} from '@pages/reference/bans/history-format.service
|
|||
templateUrl: './appeal.component.html',
|
||||
styleUrl: './appeal.component.scss'
|
||||
})
|
||||
export class AppealComponent implements OnInit, AfterViewInit {
|
||||
export class AppealComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
public form: FormGroup<Appeal>;
|
||||
private mailService = inject(MailService);
|
||||
private historyFormatService = inject(HistoryFormatService);
|
||||
private appealsService = inject(AppealsService);
|
||||
private historyService = inject(HistoryService);
|
||||
public authService = inject(AuthService);
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private boundHandleResize: any;
|
||||
|
||||
protected form: FormGroup<Appeal>;
|
||||
protected history = signal<PunishmentHistory[] | null>(null);
|
||||
protected selectedPunishment = signal<PunishmentHistory | null>(null);
|
||||
private emails = signal<EmailEntry[]>([]);
|
||||
protected verifiedEmails = computed(() => this.emails().filter(email => email.verified));
|
||||
|
||||
constructor(
|
||||
private historyFormatService: HistoryFormatService,
|
||||
private appealApi: AppealsService,
|
||||
private historyApi: HistoryService,
|
||||
protected authService: AuthService,
|
||||
private elementRef: ElementRef,
|
||||
private renderer: Renderer2
|
||||
) {
|
||||
|
|
@ -48,6 +62,9 @@ export class AppealComponent implements OnInit, AfterViewInit {
|
|||
email: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}),
|
||||
appeal: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.minLength(10)]})
|
||||
});
|
||||
this.mailService.getUserEmails().subscribe(emails => {
|
||||
this.emails.set(emails);
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
|
@ -55,7 +72,7 @@ export class AppealComponent implements OnInit, AfterViewInit {
|
|||
if (uuid === null) {
|
||||
throw new Error('JWT subject is null, are you logged in?');
|
||||
}
|
||||
this.historyApi.getAllHistoryForUUID(uuid).subscribe(history => {
|
||||
this.historyService.getAllHistoryForUUID(uuid).subscribe(history => {
|
||||
this.history.set(history.filter(item => this.historyFormatService.isActive(item)));
|
||||
})
|
||||
}
|
||||
|
|
@ -149,7 +166,7 @@ export class AppealComponent implements OnInit, AfterViewInit {
|
|||
username: this.authService.username()!,
|
||||
uuid: uuid
|
||||
}
|
||||
this.appealApi.submitMinecraftAppeal(appeal).subscribe()
|
||||
this.appealsService.submitMinecraftAppeal(appeal).subscribe()
|
||||
}
|
||||
|
||||
public currentPageIndex: number = 0;
|
||||
|
|
|
|||
48
frontend/src/app/pages/forms/sent/sent.component.html
Normal file
48
frontend/src/app/pages/forms/sent/sent.component.html
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<h2 mat-dialog-title>Email Verification</h2>
|
||||
<div mat-dialog-content>
|
||||
<p>Please enter the 6-character verification code sent to: <strong>{{ email }}</strong></p>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Verification Code</mat-label>
|
||||
<input matInput formControlName="code" placeholder="Enter 6-character code">
|
||||
@if (form.controls.code.invalid && form.controls.code.touched) {
|
||||
<mat-error>
|
||||
@if (form.controls.code.errors?.['required']) {
|
||||
Verification code is required
|
||||
} @else if (form.controls.code.errors?.['minlength'] || form.controls.code.errors?.['maxlength']) {
|
||||
Code must be exactly 6 characters
|
||||
} @else {
|
||||
Please enter the 6-character code we sent you.
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
@if (mailVerified()) {
|
||||
<p class="success-message">Email verified successfully!</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
|
||||
<button mat-button
|
||||
color="accent"
|
||||
(click)="onResend()"
|
||||
[disabled]="resendCooldown()">
|
||||
@if (resendCooldown()) {
|
||||
Resend ({{ cooldownSeconds() }}s)
|
||||
} @else {
|
||||
Resend Code
|
||||
}
|
||||
</button>
|
||||
|
||||
<button mat-flat-button
|
||||
color="primary"
|
||||
(click)="onSubmit()"
|
||||
[disabled]="form.invalid">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
23
frontend/src/app/pages/forms/sent/sent.component.scss
Normal file
23
frontend/src/app/pages/forms/sent/sent.component.scss
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
min-height: 120px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: #4caf50;
|
||||
font-weight: 500;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
149
frontend/src/app/pages/forms/sent/sent.component.ts
Normal file
149
frontend/src/app/pages/forms/sent/sent.component.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import {Component, inject, Input, input, signal} from '@angular/core';
|
||||
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {MatInput, MatLabel} from '@angular/material/input';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MailService, SubmitEmail, VerifyCode} from '@api';
|
||||
import {AuthService} from '@services/auth.service';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle} from '@angular/material/dialog';
|
||||
import {interval, Subscription} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sent',
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInput,
|
||||
MatLabel,
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions
|
||||
],
|
||||
templateUrl: './sent.component.html',
|
||||
styleUrl: './sent.component.scss'
|
||||
})
|
||||
export class SentComponent {
|
||||
protected form: FormGroup<VerifyMail>;
|
||||
|
||||
protected readonly completionMessage = input<string>("Thank you for completing your form!");
|
||||
protected readonly verifyMail = input<VerifyMailData | null>(null);
|
||||
protected mailVerified = signal<boolean>(false);
|
||||
|
||||
// For resend cooldown
|
||||
protected resendCooldown = signal<boolean>(false);
|
||||
protected cooldownSeconds = signal<number>(60);
|
||||
private cooldownSubscription: Subscription | null = null;
|
||||
|
||||
private mailService = inject(MailService);
|
||||
private authService = inject(AuthService);
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<SentComponent>,
|
||||
@Input() public email: string
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
code: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(6), Validators.maxLength(6)]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
if (this.form === undefined) {
|
||||
console.error('Form is undefined');
|
||||
return;
|
||||
}
|
||||
if (this.form.valid) {
|
||||
this.sendForm();
|
||||
} else {
|
||||
Object.keys(this.form.controls).forEach(field => {
|
||||
const control = this.form!.get(field);
|
||||
control?.markAsTouched({onlySelf: true});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onCancel() {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
public onResend() {
|
||||
if (this.resendCooldown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const submitEmail: SubmitEmail = {
|
||||
email: this.email
|
||||
};
|
||||
|
||||
this.mailService.resendVerificationEmail(submitEmail).subscribe({
|
||||
next: (response) => {
|
||||
// Start cooldown timer
|
||||
this.startResendCooldown();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error resending verification email', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startResendCooldown() {
|
||||
this.resendCooldown.set(true);
|
||||
this.cooldownSeconds.set(60);
|
||||
|
||||
if (this.cooldownSubscription) {
|
||||
this.cooldownSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.cooldownSubscription = interval(1000).subscribe(() => {
|
||||
const currentSeconds = this.cooldownSeconds();
|
||||
if (currentSeconds <= 1) {
|
||||
this.resendCooldown.set(false);
|
||||
this.cooldownSubscription?.unsubscribe();
|
||||
this.cooldownSubscription = null;
|
||||
} else {
|
||||
this.cooldownSeconds.set(currentSeconds - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private sendForm() {
|
||||
const rawValue = this.form.getRawValue();
|
||||
if (this.authService.isAuthenticated$()) {
|
||||
const form: VerifyCode = {
|
||||
code: rawValue.code,
|
||||
};
|
||||
this.mailService.verifyEmailCode(form).subscribe({
|
||||
next: (mailResponse) => {
|
||||
this.mailVerified.set(mailResponse.verified);
|
||||
if (mailResponse.verified) {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error verifying email code', error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error('User not logged in');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.cooldownSubscription) {
|
||||
this.cooldownSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface VerifyMail {
|
||||
code: FormControl<string>;
|
||||
}
|
||||
|
||||
interface VerifyMailData {
|
||||
verified: boolean;
|
||||
mail: string;
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
<li><a href="https://alttd.com/blog/">Blog</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (!isAuthenticated) {
|
||||
@if (!isAuthenticated()) {
|
||||
<li>
|
||||
<a (click)="openLoginDialog()">
|
||||
Login
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
<li class="nav_li"><a class="nav_link2" target="_blank" href="https://alttd.com/blog/">Blog</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (isAuthenticated) {
|
||||
@if (isAuthenticated()) {
|
||||
<li class="nav_li">
|
||||
<a [id]="getCurrentPageId(['particles'])"
|
||||
class="nav_link fake_link" [ngClass]="active">Special</a>
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
</ul>
|
||||
</li>
|
||||
}
|
||||
@if (!isAuthenticated) {
|
||||
@if (!isAuthenticated()) {
|
||||
<li class="nav_li login-button">
|
||||
<a class="nav_link fake_link" (click)="openLoginDialog()">
|
||||
Login
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Component, HostListener, inject, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Component, computed, HostListener, inject, Input, OnDestroy, Signal} from '@angular/core';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {ThemeComponent} from '@shared-components/theme/theme.component';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
|
@ -19,7 +19,7 @@ import {MatDialog} from '@angular/material/dialog';
|
|||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss']
|
||||
})
|
||||
export class HeaderComponent implements OnInit, OnDestroy {
|
||||
export class HeaderComponent implements OnDestroy {
|
||||
|
||||
private authService: AuthService = inject(AuthService)
|
||||
private dialog: MatDialog = inject(MatDialog)
|
||||
|
|
@ -32,14 +32,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
|||
public active: string = '';
|
||||
public inverseYPos: number = 0;
|
||||
private subscription: Subscription | undefined;
|
||||
public isAuthenticated: boolean = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subscription = this.authService.isAuthenticated$.subscribe(isAuthenticated => {
|
||||
this.isAuthenticated = isAuthenticated;
|
||||
}
|
||||
);
|
||||
}
|
||||
public isAuthenticated: Signal<boolean> = computed(() => this.authService.isAuthenticated$());
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {Injectable, signal} from '@angular/core';
|
||||
import {LoginService} from '@api';
|
||||
import {BehaviorSubject, Observable, throwError} from 'rxjs';
|
||||
import {Observable, throwError} from 'rxjs';
|
||||
import {catchError, tap} from 'rxjs/operators';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {JwtHelperService} from '@auth0/angular-jwt';
|
||||
|
|
@ -10,11 +10,10 @@ import {JwtClaims} from '@custom-types/jwt_interface'
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
|
||||
public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
|
||||
private isAuthenticatedSubject = signal<boolean>(false);
|
||||
public readonly isAuthenticated$ = this.isAuthenticatedSubject.asReadonly();
|
||||
|
||||
private userClaimsSubject = new BehaviorSubject<JwtClaims | null>(null);
|
||||
public userClaims$ = this.userClaimsSubject.asObservable();
|
||||
private userClaimsSubject = signal<JwtClaims | null>(null);
|
||||
private jwtHelper = new JwtHelperService();
|
||||
private _username = signal<string | null>(null);
|
||||
public readonly username = this._username.asReadonly();
|
||||
|
|
@ -34,7 +33,7 @@ export class AuthService {
|
|||
return this.loginService.login(code).pipe(
|
||||
tap(jwt => {
|
||||
this.saveJwt(jwt);
|
||||
this.isAuthenticatedSubject.next(true);
|
||||
this.isAuthenticatedSubject.set(true);
|
||||
|
||||
this.reloadUsername();
|
||||
}),
|
||||
|
|
@ -61,8 +60,8 @@ export class AuthService {
|
|||
*/
|
||||
public logout(): void {
|
||||
localStorage.removeItem('jwt');
|
||||
this.isAuthenticatedSubject.next(false);
|
||||
this.userClaimsSubject.next(null);
|
||||
this.isAuthenticatedSubject.set(false);
|
||||
this.userClaimsSubject.set(null);
|
||||
this._username.set(null);
|
||||
}
|
||||
|
||||
|
|
@ -84,8 +83,8 @@ export class AuthService {
|
|||
|
||||
const claims = this.extractJwtClaims(jwt);
|
||||
console.log("User claims: ", claims);
|
||||
this.userClaimsSubject.next(claims);
|
||||
this.isAuthenticatedSubject.next(true);
|
||||
this.userClaimsSubject.set(claims);
|
||||
this.isAuthenticatedSubject.set(true);
|
||||
if (this.username() == null) {
|
||||
this.reloadUsername();
|
||||
}
|
||||
|
|
@ -111,7 +110,7 @@ export class AuthService {
|
|||
|
||||
const claims = this.extractJwtClaims(jwt);
|
||||
console.log("Saving user claims: ", claims);
|
||||
this.userClaimsSubject.next(claims);
|
||||
this.userClaimsSubject.set(claims);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -125,7 +124,7 @@ export class AuthService {
|
|||
* Get user authorizations from claims
|
||||
*/
|
||||
public getUserAuthorizations(): string[] {
|
||||
const claims = this.userClaimsSubject.getValue();
|
||||
const claims = this.userClaimsSubject();
|
||||
return claims?.authorities || [];
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +134,7 @@ export class AuthService {
|
|||
}
|
||||
|
||||
public getUuid(): string | null {
|
||||
const jwtClaims = this.userClaimsSubject.getValue();
|
||||
const jwtClaims = this.userClaimsSubject();
|
||||
if (jwtClaims === null) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ tags:
|
|||
description: All actions shared between forms
|
||||
- name: appeals
|
||||
description: All action related to appeals
|
||||
- name: mail
|
||||
description: All actions related to user email verification
|
||||
paths:
|
||||
/api/team/{team}:
|
||||
$ref: './schemas/team/team.yml#/getTeam'
|
||||
|
|
@ -67,3 +69,13 @@ paths:
|
|||
$ref: './schemas/particles/particles.yml#/DownloadFile'
|
||||
/api/files/download/{uuid}/{filename}:
|
||||
$ref: './schemas/particles/particles.yml#/DownloadFileForUser'
|
||||
/api/mail/submit:
|
||||
$ref: './schemas/forms/mail/mail.yml#/SubmitEmail'
|
||||
/api/mail/verify:
|
||||
$ref: './schemas/forms/mail/mail.yml#/VerifyCode'
|
||||
/api/mail/resend:
|
||||
$ref: './schemas/forms/mail/mail.yml#/ResendEmail'
|
||||
/api/mail/delete:
|
||||
$ref: './schemas/forms/mail/mail.yml#/DeleteEmail'
|
||||
/api/mail/list:
|
||||
$ref: './schemas/forms/mail/mail.yml#/GetEmails'
|
||||
|
|
|
|||
166
open_api/src/main/resources/schemas/forms/mail/mail.yml
Normal file
166
open_api/src/main/resources/schemas/forms/mail/mail.yml
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
SubmitEmail:
|
||||
post:
|
||||
tags:
|
||||
- mail
|
||||
summary: Submit an email for verification
|
||||
description: Store a new email for the authenticated user and send a verification code
|
||||
operationId: submitEmailForVerification
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SubmitEmail'
|
||||
responses:
|
||||
'201':
|
||||
description: Verification email sent
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MailResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../generic/errors.yml#/components/schemas/ApiError'
|
||||
VerifyCode:
|
||||
post:
|
||||
tags:
|
||||
- mail
|
||||
summary: Verify an email using a code
|
||||
description: Verify the email for the authenticated user by providing the received code
|
||||
operationId: verifyEmailCode
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/VerifyCode'
|
||||
responses:
|
||||
'200':
|
||||
description: Email verified successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MailResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../generic/errors.yml#/components/schemas/ApiError'
|
||||
ResendEmail:
|
||||
post:
|
||||
tags:
|
||||
- mail
|
||||
summary: Resend verification email
|
||||
description: Request a new verification email to be sent for a pending email
|
||||
operationId: resendVerificationEmail
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SubmitEmail'
|
||||
responses:
|
||||
'200':
|
||||
description: Verification email resent
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MailResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../generic/errors.yml#/components/schemas/ApiError'
|
||||
DeleteEmail:
|
||||
delete:
|
||||
tags:
|
||||
- mail
|
||||
summary: Delete an email
|
||||
description: Delete an email associated with the authenticated user
|
||||
operationId: deleteEmail
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SubmitEmail'
|
||||
responses:
|
||||
'204':
|
||||
description: Email deleted
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../generic/errors.yml#/components/schemas/ApiError'
|
||||
GetEmails:
|
||||
get:
|
||||
tags:
|
||||
- mail
|
||||
summary: Get all emails for the authenticated user
|
||||
description: Returns both verified and unverified emails for the authenticated user
|
||||
operationId: getUserEmails
|
||||
responses:
|
||||
'200':
|
||||
description: Emails retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/EmailEntry'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../generic/errors.yml#/components/schemas/ApiError'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
SubmitEmail:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
description: Email address to verify
|
||||
VerifyCode:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: Verification code received by email
|
||||
MailResponse:
|
||||
type: object
|
||||
required:
|
||||
- message
|
||||
- email
|
||||
- verified
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
verified:
|
||||
type: boolean
|
||||
EmailEntry:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
- verified
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
description: The user's email address
|
||||
verified:
|
||||
type: boolean
|
||||
description: Whether the email has been verified
|
||||
Loading…
Reference in New Issue
Block a user