Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 38 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,34 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'

// Request DTO validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// API authentication and authorization
implementation 'org.springframework.boot:spring-boot-starter-security'

// RabbitMQ messaging
implementation 'org.springframework.boot:spring-boot-starter-amqp'

// Health checks and operational endpoints
implementation 'org.springframework.boot:spring-boot-starter-actuator'

// Database migration
implementation 'org.springframework.boot:spring-boot-flyway'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'

// OpenAPI documentation
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3'

// S3 and MinIO object storage
implementation platform('software.amazon.awssdk:bom:2.37.3')
implementation 'software.amazon.awssdk:s3'

// JWT creation and validation
implementation 'com.nimbusds:nimbus-jose-jwt:10.8'

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -42,9 +70,19 @@ dependencies {

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// Architecture rule tests
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'

// Integration test containers
testImplementation platform('org.testcontainers:testcontainers-bom:1.21.3')
testImplementation 'org.testcontainers:postgresql'
testImplementation 'org.testcontainers:rabbitmq'

testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
useJUnitPlatform()
systemProperty 'spring.profiles.active', 'test'
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan
public class StackupApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.stackup.stackup.auth.application;

import com.stackup.stackup.auth.infrastructure.GithubOAuthClient;
import com.stackup.stackup.auth.application.dto.GithubCallbackResult;
import com.stackup.stackup.auth.application.dto.GithubLoginResult;
import com.stackup.stackup.auth.application.dto.RefreshTokenResult;
import com.stackup.stackup.auth.application.dto.StreamTokenResult;
import com.stackup.stackup.common.security.JwtTokenProvider;
import com.stackup.stackup.common.security.StreamTokenProvider;
import java.util.UUID;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Service;

@Service
public class AuthService {

private static final long SKELETON_USER_ID = 1L;
private static final String SKELETON_GITHUB_USERNAME = "stackup-user";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 임시로 작성한거겠죠?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흐름 테스트를 위해서 임시 값 작성하고 나중에 로직 구현시에 없앨 예정입니다.


private final GithubOAuthClient githubOAuthClient;
private final JwtTokenProvider jwtTokenProvider;
private final StreamTokenProvider streamTokenProvider;

public AuthService(
GithubOAuthClient githubOAuthClient,
JwtTokenProvider jwtTokenProvider,
StreamTokenProvider streamTokenProvider
) {
this.githubOAuthClient = githubOAuthClient;
this.jwtTokenProvider = jwtTokenProvider;
this.streamTokenProvider = streamTokenProvider;
}

public GithubLoginResult startGithubLogin() {
String state = UUID.randomUUID().toString();
return new GithubLoginResult(githubOAuthClient.buildAuthorizationUrl(state), state);
}

public GithubCallbackResult completeGithubLogin(String code, String state) {
return new GithubCallbackResult(
jwtTokenProvider.createAccessToken(SKELETON_USER_ID),
createRefreshTokenStub(),
SKELETON_USER_ID,
SKELETON_GITHUB_USERNAME
);
}

public RefreshTokenResult refresh(String refreshToken) {
return new RefreshTokenResult(jwtTokenProvider.createAccessToken(SKELETON_USER_ID), createRefreshTokenStub());
}

public void logout(String refreshToken) {
// Refresh token revocation is intentionally deferred to the auth implementation task.
}

public StreamTokenResult createStreamToken(Long userId) {
if (userId == null) {
throw new BadCredentialsException("Authentication is required to create stream token");
}
return new StreamTokenResult(streamTokenProvider.createStreamToken(userId));
}

private String createRefreshTokenStub() {
return "refresh-token-stub-" + UUID.randomUUID();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stackup.stackup.auth.application.dto;

public record GithubCallbackResult(
String accessToken,
String refreshToken,
long userId,
String githubUsername
) {
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record 좋네요 코드 간결 굿

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.stackup.stackup.auth.application.dto;

public record GithubLoginResult(
String authorizationUrl,
String state
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.stackup.stackup.auth.application.dto;

public record RefreshTokenResult(
String accessToken,
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.stackup.stackup.auth.application.dto;

public record StreamTokenResult(
String streamToken
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.time.Instant;

@Getter
@Entity
Expand Down Expand Up @@ -46,7 +46,7 @@ public class RefreshToken extends BaseTimeEntity {
private String deviceInfo;

@Column(name = "expires_at", nullable = false)
private LocalDateTime expiresAt;
private Instant expiresAt;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문] 오 Instant는 어떤 차이가 있나요. 시간대 표현 시 저 방식이 더 효과적일까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 LocalDatetime 자체가 상대적으로 표시가 될 수 있어서
UTC 같은 시간대를 표시한 Instant를 통해 절대적인 시간 계산이 돼서 토큰 관리가 효과적일 거 같습니다.


@Column(name = "is_revoked", nullable = false)
private boolean revoked = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.stackup.stackup.auth.domain;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

Optional<RefreshToken> findByTokenHash(String tokenHash);

void deleteByUser_Id(Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.stackup.stackup.auth.infrastructure;

import com.stackup.stackup.common.config.properties.GithubOAuthProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

@Component
public class GithubOAuthClient {

private final GithubOAuthProperties githubOAuthProperties;

public GithubOAuthClient(GithubOAuthProperties githubOAuthProperties) {
this.githubOAuthProperties = githubOAuthProperties;
}

public String buildAuthorizationUrl(String state) {
return UriComponentsBuilder.fromUriString("https://github.com/login/oauth/authorize")
.queryParam("client_id", githubOAuthProperties.clientId())
.queryParam("redirect_uri", githubOAuthProperties.redirectUri())
.queryParam("scope", "read:user user:email repo")
.queryParam("state", state)
.build()
.toUriString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.stackup.stackup.auth.presentation;

import com.stackup.stackup.auth.application.AuthService;
import com.stackup.stackup.auth.application.dto.GithubCallbackResult;
import com.stackup.stackup.auth.application.dto.GithubLoginResult;
import com.stackup.stackup.auth.application.dto.RefreshTokenResult;
import com.stackup.stackup.auth.application.dto.StreamTokenResult;
import com.stackup.stackup.auth.presentation.dto.GithubAuthRequest;
import com.stackup.stackup.auth.presentation.dto.GithubCallbackResponse;
import com.stackup.stackup.auth.presentation.dto.GithubLoginResponse;
import com.stackup.stackup.auth.presentation.dto.LogoutRequest;
import com.stackup.stackup.auth.presentation.dto.RefreshTokenRequest;
import com.stackup.stackup.auth.presentation.dto.RefreshTokenResponse;
import com.stackup.stackup.auth.presentation.dto.StreamTokenResponse;
import com.stackup.stackup.common.security.UserPrincipal;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

private final AuthService authService;

public AuthController(AuthService authService) {
this.authService = authService;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 보다는 자동 DI 사용하는편이 좋아보입니다.
스프링이 알아서 DI 해줄거에요 저거 안써도

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 다른 방법이 있군요 한번 찾아보겠습니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

찾아본결과 record클래스로 변경하는거 같아서 controller를 record로 변경했습니다.

}

@PostMapping("/github")
public ResponseEntity<GithubLoginResponse> startGithubLogin(@RequestBody(required = false) GithubAuthRequest request) {
GithubLoginResult result = authService.startGithubLogin();
return ResponseEntity.ok(new GithubLoginResponse(result.authorizationUrl(), result.state()));
}

@GetMapping("/github/callback")
public ResponseEntity<GithubCallbackResponse> githubCallback(
@RequestParam(required = false) String code,
@RequestParam(required = false) String state
) {
GithubCallbackResult result = authService.completeGithubLogin(code, state);
return ResponseEntity.ok(new GithubCallbackResponse(
result.accessToken(),
result.refreshToken(),
result.userId(),
result.githubUsername()
));
}

@PostMapping("/refresh")
public ResponseEntity<RefreshTokenResponse> refresh(@RequestBody(required = false) RefreshTokenRequest request) {
String refreshToken = request == null ? null : request.refreshToken();
RefreshTokenResult result = authService.refresh(refreshToken);
return ResponseEntity.ok(new RefreshTokenResponse(result.accessToken(), result.refreshToken()));
}

@DeleteMapping("/logout")
public ResponseEntity<Void> logout(@RequestBody(required = false) LogoutRequest request) {
String refreshToken = request == null ? null : request.refreshToken();
authService.logout(refreshToken);
return ResponseEntity.noContent().build();
}

@PostMapping("/stream-token")
public ResponseEntity<StreamTokenResponse> createStreamToken(
@AuthenticationPrincipal UserPrincipal principal
) {
Long userId = principal == null ? null : principal.userId();
StreamTokenResult result = authService.createStreamToken(userId);
return ResponseEntity.ok(new StreamTokenResponse(result.streamToken()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.stackup.stackup.auth.presentation.dto;

public record GithubAuthRequest(
String code,
String state
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stackup.stackup.auth.presentation.dto;

public record GithubCallbackResponse(
String accessToken,
String refreshToken,
long userId,
String githubUsername
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.stackup.stackup.auth.presentation.dto;

public record GithubLoginResponse(
String authorizationUrl,
String state
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.stackup.stackup.auth.presentation.dto;

public record LogoutRequest(
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.stackup.stackup.auth.presentation.dto;

public record RefreshTokenRequest(
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.stackup.stackup.auth.presentation.dto;

public record RefreshTokenResponse(
String accessToken,
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.stackup.stackup.auth.presentation.dto;

public record StreamTokenResponse(
String streamToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.stackup.stackup.common.config.properties;

import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "app.cors")
public record CorsProperties(
@NotEmpty List<String> allowedOrigins
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.stackup.stackup.common.config.properties;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.net.URI;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "app.github")
public record GithubOAuthProperties(
@NotBlank String clientId,
@NotBlank String clientSecret,
@NotNull
URI redirectUri
) {
}
Loading