Skip to content

Commit

Permalink
Merge pull request #372 from woowacourse-teams/develop
Browse files Browse the repository at this point in the history
[ALL] 모모의 두번째 배포가 무사하길 기원해요
  • Loading branch information
hw0603 authored Sep 27, 2024
2 parents 19ead22 + ea2ccdc commit 59285ff
Show file tree
Hide file tree
Showing 179 changed files with 5,481 additions and 1,220 deletions.
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'

implementation 'org.springframework.security:spring-security-crypto'
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

Expand Down
56 changes: 56 additions & 0 deletions backend/src/main/java/kr/momo/config/DataSourceConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package kr.momo.config;

import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;

@Configuration
@Profile("prod")
public class DataSourceConfig {

public static final String SOURCE_SERVER = "source";
public static final String REPLICA_SERVER = "replica";

@Bean(SOURCE_SERVER)
@ConfigurationProperties(prefix = "spring.datasource.source")
public DataSource sourceDataSource() {
return DataSourceBuilder.create()
.build();
}

@Bean(REPLICA_SERVER)
@ConfigurationProperties(prefix = "spring.datasource.replica")
public DataSource replicaDataSource() {
return DataSourceBuilder.create()
.build();
}

@Bean
public DataSource routingDataSource(
@Qualifier(SOURCE_SERVER) DataSource source,
@Qualifier(REPLICA_SERVER) DataSource replica
) {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> dataSourceMap = Map.of(
SOURCE_SERVER, source,
REPLICA_SERVER, replica
);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(source);
return routingDataSource;
}

@Bean
@Primary
public DataSource dataSource() {
DataSource dataSource = routingDataSource(sourceDataSource(), replicaDataSource());
return new LazyConnectionDataSourceProxy(dataSource);
}
}
15 changes: 15 additions & 0 deletions backend/src/main/java/kr/momo/config/PasswordEncoderConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.momo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordEncoderConfig {

@Bean
public PasswordEncoder passwordEncoder() {
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
}
}
15 changes: 15 additions & 0 deletions backend/src/main/java/kr/momo/config/RoutingDataSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.momo.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class RoutingDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
return DataSourceConfig.REPLICA_SERVER;
}
return DataSourceConfig.SOURCE_SERVER;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,12 @@ public ResponseEntity<Void> logout(@PathVariable String uuid) {
public MomoApiResponse<List<String>> findAttendeesOfMeeting(@PathVariable String uuid) {
return new MomoApiResponse<>(attendeeService.findAll(uuid));
}

/**
* TEMP: 비밀번호 마이그레이션 이후 삭제될 메서드입니다.
*/
@PostMapping("/api/v1/attendee/update-password")
public MomoApiResponse<Integer> updatePassword() {
return new MomoApiResponse<>(attendeeService.updateAllPassword());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public interface MeetingControllerDocs {
@Operation(summary = "약속 생성", description = "주최자가 약속을 생성하는 API 입니다.")
@ApiSuccessResponse.Created("약속 생성 성공")
@ApiErrorResponse.BadRequest(ERROR_CODE_TABLE_HEADER + """
| INVALID_FORMAT_REQUEST | 유효하지 않은 요청입니다 |
| INVALID_NAME_LENGTH | 이름 길이는 1자 이상 5자 이하 까지 가능합니다. |
| INVALID_PASSWORD_LENGTH | 비밀번호 길이는 1자 이상 10자 이하 까지 가능합니다. |
| PAST_NOT_PERMITTED | 과거 날짜로는 약속을 생성할 수 없습니다. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import kr.momo.controller.auth.AuthAttendee;
import kr.momo.service.schedule.ScheduleService;
import kr.momo.service.schedule.dto.AttendeeScheduleResponse;
import kr.momo.service.schedule.dto.RecommendedScheduleResponse;
import kr.momo.service.schedule.dto.RecommendedSchedulesResponse;
import kr.momo.service.schedule.dto.ScheduleCreateRequest;
import kr.momo.service.schedule.dto.SchedulesResponse;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -51,10 +51,10 @@ public MomoApiResponse<AttendeeScheduleResponse> findMySchedule(@PathVariable St
}

@GetMapping("/api/v1/meetings/{uuid}/recommended-schedules")
public MomoApiResponse<List<RecommendedScheduleResponse>> recommendSchedules(
public MomoApiResponse<RecommendedSchedulesResponse> recommendSchedules(
@PathVariable String uuid, @RequestParam String recommendType, @RequestParam List<String> attendeeNames
) {
List<RecommendedScheduleResponse> response = scheduleService.recommendSchedules(
RecommendedSchedulesResponse response = scheduleService.recommendSchedules(
uuid, recommendType, attendeeNames
);
return new MomoApiResponse<>(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import kr.momo.controller.annotation.ApiSuccessResponse;
import kr.momo.controller.auth.AuthAttendee;
import kr.momo.service.schedule.dto.AttendeeScheduleResponse;
import kr.momo.service.schedule.dto.RecommendedScheduleResponse;
import kr.momo.service.schedule.dto.RecommendedSchedulesResponse;
import kr.momo.service.schedule.dto.ScheduleCreateRequest;
import kr.momo.service.schedule.dto.SchedulesResponse;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -90,13 +90,13 @@ MomoApiResponse<AttendeeScheduleResponse> findMySchedule(
추천 기준에 따라 이른 시간 순 혹은 길게 볼 수 있는 순으로 추천합니다.
- earliest: 이른 시간 순
- longTerm: 길게 볼 수 있는 순
추천 연산에 사용할 참여자 이름을 명시하여 필터링할 수 있습니다.<br>
약속 내의 모든 참여자가 전달된 경우 일부 참여자들이 참여할 수 있는 일정을 함께 추천하며,<br>
이외의 경우 전달된 참여자들이 모두 참여할 수 있는 일정이 추천됩니다.
""")
@ApiSuccessResponse.Ok("추천 일정 조회 성공")
MomoApiResponse<List<RecommendedScheduleResponse>> recommendSchedules(
MomoApiResponse<RecommendedSchedulesResponse> recommendSchedules(
@PathVariable @Schema(description = "약속 UUID") String uuid,
@RequestParam @Schema(description = "추천 기준(이른 시간 순 / 길게 볼 수 있는 순)", example = "earliest")
String recommendType,
Expand Down
21 changes: 11 additions & 10 deletions backend/src/main/java/kr/momo/domain/attendee/Attendee.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;

@Table(name = "attendee")
@Entity
Expand Down Expand Up @@ -46,17 +47,17 @@ public class Attendee extends BaseEntity {
@Column(nullable = false, length = 10)
private Role role;

public Attendee(Meeting meeting, String name, AttendeePassword password, Role role) {
this(meeting, new AttendeeName(name), password, role);
}

public Attendee(Meeting meeting, AttendeeName name, AttendeePassword password, Role role) {
this.meeting = meeting;
this.name = name;
this.password = password;
this.role = role;
}

public Attendee(Meeting meeting, String name, String password, Role role) {
this(meeting, new AttendeeName(name), new AttendeePassword(password), role);
}

public boolean isHost() {
return role.isHost();
}
Expand All @@ -65,15 +66,15 @@ public boolean isNotHost() {
return !isHost();
}

public void verifyPassword(AttendeePassword other) {
this.password.verifyPassword(other);
public void updatePassword(String password) {
this.password = new AttendeePassword(password);
}

public String name() {
return this.name.getName();
public void verifyPassword(AttendeeRawPassword rawPassword, PasswordEncoder passwordEncoder) {
password.verifyMatch(rawPassword, passwordEncoder);
}

public String password() {
return this.password.getPassword();
public String name() {
return name.getName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,27 @@

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.util.regex.Pattern;
import kr.momo.exception.MomoException;
import kr.momo.exception.code.AttendeeErrorCode;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;

@Embeddable
@Getter
@EqualsAndHashCode
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AttendeePassword {

private static final Pattern PASSWORD_PATTERN = Pattern.compile("^[a-zA-Z0-9!@*#$%]+$");

@Column(nullable = false, length = 10)
@Column(nullable = false)
private String password;

public AttendeePassword(String password) {
validatePassword(password);
this.password = password;
}

private void validatePassword(String password) {
validatePasswordLength(password);
validatePasswordFormat(password);
}

private void validatePasswordLength(String password) {
if (password.length() > 10) {
throw new MomoException(AttendeeErrorCode.INVALID_PASSWORD_LENGTH);
}
}

private void validatePasswordFormat(String password) {
if (!PASSWORD_PATTERN.matcher(password).matches()) {
throw new MomoException(AttendeeErrorCode.INVALID_PASSWORD_FORMAT);
}
}

public void verifyPassword(AttendeePassword other) {
if (!this.equals(other)) {
public void verifyMatch(AttendeeRawPassword rawPassword, PasswordEncoder passwordEncoder) {
if (!passwordEncoder.matches(rawPassword.password(), password)) {
throw new MomoException(AttendeeErrorCode.PASSWORD_MISMATCHED);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.momo.domain.attendee;

import java.util.regex.Pattern;
import kr.momo.exception.MomoException;
import kr.momo.exception.code.AttendeeErrorCode;
import org.springframework.security.crypto.password.PasswordEncoder;

public record AttendeeRawPassword(String password) {

private static final Pattern PASSWORD_PATTERN = Pattern.compile("^\\d{4}+$");

public AttendeeRawPassword {
validatePassword(password);
}

private void validatePassword(String password) {
if (password == null || !PASSWORD_PATTERN.matcher(password).matches()) {
throw new MomoException(AttendeeErrorCode.INVALID_PASSWORD_FORMAT);
}
}

public AttendeePassword encodePassword(PasswordEncoder passwordEncoder) {
return new AttendeePassword(passwordEncoder.encode(password));
}
}
10 changes: 10 additions & 0 deletions backend/src/main/java/kr/momo/domain/meeting/ConfirmedMeeting.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -65,11 +66,20 @@ public AttendeeGroup availableAttendeesOf(List<Schedule> schedules) {
}

private boolean isScheduleWithinDateTimeRange(Schedule schedule) {
if (meeting.isDaysOnly()) {
LocalDate date = schedule.date();
LocalDate startDate = startDateTime.toLocalDate();
LocalDate endDate = endDateTime.toLocalDate();
return !date.isBefore(startDate) && !date.isAfter(endDate);
}
LocalDateTime dateTime = schedule.dateTime();
return !dateTime.isBefore(startDateTime) && dateTime.isBefore(endDateTime);
}

private long countTimeSlotOfConfirmedMeeting() {
if (meeting.isDaysOnly()) {
return Duration.between(startDateTime, endDateTime).plusDays(1).toDays();
}
return Duration.between(startDateTime, endDateTime).dividedBy(SECOND_OF_HALF_HOUR).getSeconds();
}
}
19 changes: 17 additions & 2 deletions backend/src/main/java/kr/momo/domain/meeting/Meeting.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
Expand Down Expand Up @@ -34,14 +36,23 @@ public class Meeting extends BaseEntity {
@Column(nullable = false)
private boolean isLocked;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 10)
private MeetingType type;

@Embedded
private TimeslotInterval timeslotInterval;

public Meeting(String name, String uuid, LocalTime firstTime, LocalTime lastTime) {
public Meeting(String name, String uuid, LocalTime firstTime, LocalTime lastTime, MeetingType type) {
this.name = name;
this.uuid = uuid;
this.timeslotInterval = new TimeslotInterval(firstTime, lastTime.minusMinutes(30));
this.isLocked = false;
this.type = type;
this.timeslotInterval = new TimeslotInterval(firstTime, lastTime.minusMinutes(30));
}

public Meeting(String name, String uuid, LocalTime firstTime, LocalTime lastTime) {
this(name, uuid, firstTime, lastTime, MeetingType.DATETIME);
}

public void lock() {
Expand All @@ -60,6 +71,10 @@ public boolean isNotFullTime() {
return timeslotInterval.isNotFullTime();
}

public boolean isDaysOnly() {
return type.isDaysOnly();
}

public Timeslot getValidatedTimeslot(LocalTime other) {
return timeslotInterval.getValidatedTimeslot(other);
}
Expand Down
24 changes: 24 additions & 0 deletions backend/src/main/java/kr/momo/domain/meeting/MeetingType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kr.momo.domain.meeting;

import com.fasterxml.jackson.annotation.JsonCreator;
import kr.momo.exception.MomoException;
import kr.momo.exception.code.MeetingErrorCode;

public enum MeetingType {

DAYSONLY,
DATETIME;

public boolean isDaysOnly() {
return this.equals(DAYSONLY);
}

@JsonCreator
public static MeetingType from(String type) {
try {
return MeetingType.valueOf(type.toUpperCase());
} catch (IllegalArgumentException | NullPointerException e) {
throw new MomoException(MeetingErrorCode.INVALID_TYPE);
}
}
}
Loading

0 comments on commit 59285ff

Please sign in to comment.