Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ac57fb0
[feat] #34 서버 사용 신청
chaehyeo-n Aug 12, 2025
9160936
[feat] #34 서버 사용 신청 목록 조회 (관리자용)
chaehyeo-n Aug 12, 2025
db3a172
[feat] #34 서버 사용 신청 승인
chaehyeo-n Aug 12, 2025
e4f0ef7
[feat] #34 서버 사용 신청 거절
chaehyeo-n Aug 12, 2025
53f66ed
[feat] #34 서버 사용 승인 시 Slack DM & Email 전송
chaehyeo-n Aug 12, 2025
fcc769b
[fix] #34 request response에 cudaversion 삭제
chaehyeo-n Aug 12, 2025
b576291
[feat] #34 request swagger 정리
chaehyeo-n Aug 12, 2025
538da87
[feat] 이미지 생성/조회
chaehyeo-n Aug 12, 2025
0c2e4dd
[fix] request response 입력/반환값 수정
chaehyeo-n Aug 13, 2025
035b42d
[feat] 사용자 SSH 인증
chaehyeo-n Aug 13, 2025
3f96ea9
Merge branch 'develop' into feat/#34-request
chaehyeo-n Aug 13, 2025
8265816
Merge pull request #76 from CSID-DGU/feat/#34-request
saokiritoni Aug 14, 2025
bc67d7c
[fix] 코드 오류 수정
chaehyeo-n Aug 14, 2025
7900002
[feat] 사용 신청 승인 시 pvc post
chaehyeo-n Aug 14, 2025
7f56c83
[fix] ubuntu username unique 설정
chaehyeo-n Aug 14, 2025
08be728
[feat] #34 ubuntu username 중복 검사
chaehyeo-n Aug 14, 2025
5b85947
[fix] #34 uid, gid 반환
chaehyeo-n Aug 14, 2025
2e151b7
[feat] #34 사용 신청 승인 시 uid, gid 할당
chaehyeo-n Aug 14, 2025
70282c9
[feat] #34 config server용 사용 신청 승인 내역 조회
chaehyeo-n Aug 15, 2025
a5c7df5
[feat] 전체/단일 pod 조회 api
chaehyeo-n Aug 15, 2025
b41160c
[fix] pod 조회 api 경로 수정
chaehyeo-n Aug 15, 2025
83bdabc
[feat] kubernetes 의존성 추가
chaehyeo-n Aug 15, 2025
8610ff3
Merge pull request #78 from CSID-DGU/feat/#34-request
chaehyeo-n Aug 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ dependencies {
// smtp
implementation 'org.springframework.boot:spring-boot-starter-mail'

// kubernetes
implementation 'io.fabric8:kubernetes-client:6.10.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package DGU_AI_LAB.admin_be.domain.alarm.controller;

import DGU_AI_LAB.admin_be.domain.alarm.controller.docs.AlarmApi;
import DGU_AI_LAB.admin_be.domain.alarm.dto.request.CombinedAlertRequestDTO;
import DGU_AI_LAB.admin_be.domain.alarm.dto.request.EmailRequestDTO;
import DGU_AI_LAB.admin_be.domain.alarm.dto.request.SlackDMRequestDTO;
import DGU_AI_LAB.admin_be.domain.alarm.service.AlarmService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/alert")
public class AlarmController implements AlarmApi {

private final AlarmService alarmService;

@PostMapping("/dm")
public ResponseEntity<String> sendSlackDMAlert(@RequestBody @Valid SlackDMRequestDTO request) {
alarmService.sendDMAlert(request.username(), request.email(), request.message());
return ResponseEntity.ok("Alert sent to Slack DM");
}

@PostMapping("/email")
public ResponseEntity<?> sendEmailAlert(@RequestBody @Valid EmailRequestDTO request) {
alarmService.sendMailAlert(request.to(), request.subject(), request.body());
return ResponseEntity.ok("Alert sent to Email");
}

@PostMapping
public ResponseEntity<String> sendAllAlerts(@RequestBody @Valid CombinedAlertRequestDTO request) {
alarmService.sendAllAlerts(
request.username(),
request.email(),
request.subject(),
request.message()
);
return ResponseEntity.ok("Alert sent to Slack DM and Email");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package DGU_AI_LAB.admin_be.domain.alarm.controller.docs;

import DGU_AI_LAB.admin_be.domain.alarm.dto.request.CombinedAlertRequestDTO;
import DGU_AI_LAB.admin_be.domain.alarm.dto.request.EmailRequestDTO;
import DGU_AI_LAB.admin_be.domain.alarm.dto.request.SlackDMRequestDTO;
import DGU_AI_LAB.admin_be.global.common.SuccessResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;

@Tag(name = "Slack 및 E-mail", description = "Slack 및 Email 알림 API")
public interface AlarmApi {

@Operation(
summary = "Slack DM 알림 전송",
description = "Slack 사용자에게 개인 DM으로 알림 메시지를 전송합니다. 이름이 중복될 경우 이메일이 일치하는 사용자에게 전송됩니다."
)
@ApiResponse(
responseCode = "200", description = "Slack DM 전송 성공",
content = @Content(schema = @Schema(implementation = SuccessResponse.class))
)
ResponseEntity<?> sendSlackDMAlert(
@RequestBody(description = "Slack DM 알림 요청 DTO", required = true)
@Valid SlackDMRequestDTO request
);

@Operation(
summary = "Email 알림 전송",
description = "Email로 알림 메시지를 전송합니다."
)
@ApiResponse(
responseCode = "200", description = "Email 전송 성공",
content = @Content(schema = @Schema(implementation = SuccessResponse.class))
)
ResponseEntity<?> sendEmailAlert(
@RequestBody(description = "이메일 알림 요청 DTO", required = true)
@Valid EmailRequestDTO request
);

@Operation(
summary = "Slack DM + Email 통합 알림 전송",
description = "Slack DM과 Email을 동시에 전송합니다."
)
@ApiResponse(
responseCode = "200", description = "Slack + Email 알림 전송 성공",
content = @Content(schema = @Schema(implementation = SuccessResponse.class))
)
ResponseEntity<?> sendAllAlerts(
@RequestBody(description = "통합 알림 요청 DTO", required = true)
@Valid CombinedAlertRequestDTO request
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package DGU_AI_LAB.admin_be.domain.alarm.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;

@Builder
public record CombinedAlertRequestDTO(
@NotBlank
String username,

@NotBlank
@Email
String email,

@NotBlank
String subject,

@NotBlank
String message
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package DGU_AI_LAB.admin_be.domain.alarm.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;

@Builder
public record EmailRequestDTO(
@NotBlank
@Email
String to,

@NotBlank
String subject,

@NotBlank
String body
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package DGU_AI_LAB.admin_be.domain.alarm.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;

@Builder
public record SlackDMRequestDTO(
@NotBlank
String username,

@NotBlank
@Email
String email,

@NotBlank
String message
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package DGU_AI_LAB.admin_be.domain.alarm.service;

import DGU_AI_LAB.admin_be.error.exception.BusinessException;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
// import java.util.Map;
import DGU_AI_LAB.admin_be.error.ErrorCode;

@Service
@RequiredArgsConstructor
@Log4j2
public class AlarmService {

@Value("${slack-webhook-url.monitoring}")
private String defaultWebhookUrl;

@Value("${slack.bot-token}")
private String botToken;
private final RestTemplate restTemplate = new RestTemplate();

private final JavaMailSender mailSender;

@Value("${spring.mail.username}")
private String from;

public void sendSlackAlert(String message) {
sendSlackAlert(message, null);
}

public void sendSlackAlert(String message, String webhookUrl) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

Map<String, String> payload = Map.of("text", message);
HttpEntity<Map<String, String>> request = new HttpEntity<>(payload, headers);

String urlToUse = (webhookUrl != null && !webhookUrl.isEmpty()) ? webhookUrl : defaultWebhookUrl;

ResponseEntity<String> response = restTemplate.postForEntity(urlToUse, request, String.class);

if (!response.getStatusCode().is2xxSuccessful()) {
log.debug("Slack 알림 전송 실패: {}", response.getStatusCode());
} else {
log.debug("Slack 알림 전송 성공");
}
}
public void sendMailAlert(String to, String subject, String body) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(body);
mailSender.send(message);
System.out.printf("메일 전송 성공: 수신자=%s, 제목=%s%n", to, subject);
}

// slack dm 전송
public void sendDMAlert(String username, String email, String message) {
String userId = getSlackUser(username, email, botToken);
if (userId == null) {
throw new BusinessException(ErrorCode.SLACK_USER_NOT_FOUND);
}

String channelId = openDMChannel(userId, botToken);
if (channelId == null) {
throw new BusinessException(ErrorCode.SLACK_DM_CHANNEL_FAILED);
}

try {
sendMessageToSlackChannel(channelId, message, botToken);
} catch (Exception e) {
throw new BusinessException(ErrorCode.SLACK_SEND_FAILED);
}
}

// 이름이 일치하는 사용자에게 dm 전송
// 이름이 같은 사용자가 있는 경우 email이 일치하는 사용자에게 dm 전송
private String getSlackUser(String username, String email, String token) {
String url = "https://slack.com/api/users.list";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<Void> request = new HttpEntity<>(headers);

ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, request, Map.class);
if (!Boolean.TRUE.equals(response.getBody().get("ok"))) {
throw new BusinessException(ErrorCode.SLACK_USER_NOT_FOUND);
}

List<Map<String, Object>> members = (List<Map<String, Object>>) response.getBody().get("members");

// 이름이 일치하는 사용자 목록 필터링
List<Map<String, Object>> matchedUsers = members.stream()
.filter(user -> {
Map<String, Object> profile = (Map<String, Object>) user.get("profile");
String displayName = (String) profile.get("display_name");
String realName = (String) profile.get("real_name");
String name = (String) user.get("name");

return username.equals(name) || username.equals(displayName) || username.equals(realName);
})
.collect(Collectors.toList());

if (matchedUsers.isEmpty()) {
throw new BusinessException(ErrorCode.SLACK_USER_NOT_FOUND);
}

if (matchedUsers.size() == 1) {
return (String) matchedUsers.get(0).get("id");
}

Map<String, Object> selectedUser = matchedUsers.stream()
.filter(user -> {
Map<String, Object> profile = (Map<String, Object>) user.get("profile");
String userEmail = (String) profile.get("email");
return userEmail != null && userEmail.equalsIgnoreCase(email);
})
.findFirst()
.orElseThrow(() -> new BusinessException(ErrorCode.SLACK_USER_EMAIL_NOT_MATCH));

return (String) selectedUser.get("id");
}

private String openDMChannel(String userId, String token) {
String url = "https://slack.com/api/conversations.open";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);

Map<String, Object> body = Map.of("users", userId);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);

ResponseEntity<Map> response = restTemplate.postForEntity(url, request, Map.class);
if (Boolean.TRUE.equals(response.getBody().get("ok"))) {
Map channel = (Map) response.getBody().get("channel");
return (String) channel.get("id");
}
return null;
}

private void sendMessageToSlackChannel(String channelId, String message, String token) {
String url = "https://slack.com/api/chat.postMessage";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);

Map<String, Object> body = Map.of(
"channel", channelId,
"text", message
);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);

ResponseEntity<Map> response = restTemplate.postForEntity(url, request, Map.class);
if (!Boolean.TRUE.equals(response.getBody().get("ok"))) {
throw new BusinessException(ErrorCode.SLACK_SEND_FAILED);
}
}

public void sendAllAlerts(String username, String email, String subject, String message) {
sendDMAlert(username, email, message);
sendMailAlert(email, subject, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package DGU_AI_LAB.admin_be.domain.containerImage.controller;

import DGU_AI_LAB.admin_be.domain.containerImage.controller.docs.ContainerImageApi;
import DGU_AI_LAB.admin_be.domain.containerImage.dto.request.ContainerImageCreateRequest;
import DGU_AI_LAB.admin_be.domain.containerImage.dto.response.ContainerImageResponseDTO;
import DGU_AI_LAB.admin_be.domain.containerImage.service.ContainerImageService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/images")
public class ContainerImageController implements ContainerImageApi {

private final ContainerImageService containerImageService;

@PostMapping
public ResponseEntity<ContainerImageResponseDTO> createImage(
@RequestBody @Valid ContainerImageCreateRequest request
) {
ContainerImageResponseDTO createdImage = containerImageService.createImage(request);
return ResponseEntity.ok(createdImage);
}

@GetMapping("/{id}")
public ResponseEntity<ContainerImageResponseDTO> getImageById(@PathVariable Long id) {
return ResponseEntity.ok(containerImageService.getImageById(id));
}

@GetMapping
public ResponseEntity<List<ContainerImageResponseDTO>> getAllImages() {
return ResponseEntity.ok(containerImageService.getAllImages());
}
}
Loading