diff --git a/.gitignore b/.gitignore index 4275ec4..e2e0e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build/ !**/src/test/**/build/ *.yml +*.p12 !docker-compose** !.github/** @@ -39,4 +40,8 @@ out/ /.nb-gradle/ ### VS Code ### -.vscode/ \ No newline at end of file +.vscode/ + + + + diff --git a/docs/infra/local-https-setup.md b/docs/infra/local-https-setup.md new file mode 100644 index 0000000..52c87eb --- /dev/null +++ b/docs/infra/local-https-setup.md @@ -0,0 +1,148 @@ +# πŸ”’ 둜컬 HTTPS 개발 ν™˜κ²½ ꡬ성 (`local.juulabel.com`) + +Next.js ν”„λ‘ νŠΈμ—”λ“œμ™€ Spring Boot λ°±μ—”λ“œλ₯Ό μœ„ν•œ 둜컬 HTTPS 개발 ν™˜κ²½ μ„€μ • κ°€μ΄λ“œμž…λ‹ˆλ‹€. `https://local.juulabel.com` 도메인을 κΈ°μ€€μœΌλ‘œ μ–‘μͺ½ ν™˜κ²½μ„ ν†΅ν•©ν•©λ‹ˆλ‹€. + +--- + +## 🧩 1. 곡톡 호슀트 μ„€μ • + +### βœ… `/etc/hosts` 파일 μˆ˜μ • + +- **macOS / Linux**: + + ```bash + sudo nano /etc/hosts + ``` + +- **Windows**: + ``` + C:\Windows\System32\drivers\etc\hosts + ``` + +#### πŸ“Œ λ‚΄μš© μΆ”κ°€: + +``` +127.0.0.1 local.juulabel.com +``` + +--- + +## πŸ” 2. HTTPS μΈμ¦μ„œ 생성 + +### βœ… `mkcert` μ„€μΉ˜ ν›„ μΈμ¦μ„œ 생성: + +```bash +mkcert local.juulabel.com +``` + +μƒμ„±λœ 파일: + +- `local.juulabel.com.pem` – μΈμ¦μ„œ +- `local.juulabel.com-key.pem` – 개인 ν‚€ + +--- + +## 🧭 ν”„λ‘ νŠΈμ—”λ“œ (Next.js) + +### πŸ“ ν”„λ‘œμ νŠΈ λ£¨νŠΈμ— 파일 배치: + +``` +juulabel-front/ +β”œβ”€β”€ local.juulabel.com.pem +β”œβ”€β”€ local.juulabel.com-key.pem +β”œβ”€β”€ server.cjs +``` + +### 🧾 `server.cjs` + +```js +const { createServer } = require("https"); +const { parse } = require("url"); +const next = require("next"); +const fs = require("fs"); +const path = require("path"); + +const dev = process.env.NODE_ENV !== "production"; +const app = next({ dev }); +const handle = app.getRequestHandler(); + +const httpsOptions = { + key: fs.readFileSync(path.join(__dirname, "local.juulabel.com-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "local.juulabel.com.pem")), +}; + +app.prepare().then(() => { + createServer(httpsOptions, (req, res) => { + const parsedUrl = parse(req.url, true); + handle(req, res, parsedUrl); + }).listen(3000, () => { + console.log("βœ… App running at https://local.juulabel.com:3000"); + }); +}); +``` + +### πŸ“œ `package.json` μ„€μ • + +```json +"scripts": { + "dev:https": "node server.cjs" +} +``` + +μ‹€ν–‰: + +```bash +pnpm run dev:https +``` + +--- + +## πŸ›‘ λ°±μ—”λ“œ (Spring Boot) + +### πŸ“¦ μΈμ¦μ„œ λ³€ν™˜ (PKCS12) + +```bash +openssl pkcs12 -export \ + -in local.juulabel.com.pem \ + -inkey local.juulabel.com-key.pem \ + -out local.juulabel.com.p12 \ + -name local-ssl +``` + +### πŸ“ keystore 파일 μœ„μΉ˜ + +`local.juulabel.com.p12` β†’ `src/main/resources` 디렉토리에 볡사 + +--- + +### βš™οΈ `application.yml` μ„€μ • + +```yaml +server: + port: 8080 + ssl: + enabled: true + key-store: classpath:local.juulabel.com.p12 + key-store-password: your_password + key-store-type: PKCS12 +``` + +--- + +## βœ… κ²°κ³Ό μš”μ•½ + +| ν•­λͺ© | μ£Όμ†Œ | +| ---------- | ----------------------------------------- | +| ν”„λ‘ νŠΈμ—”λ“œ | `https://local.juulabel.com:3000` | +| λ°±μ—”λ“œ | `https://local.juulabel.com:8080` | +| μΏ ν‚€ | Secure + SameSite=None + 동일 도메인 ν•„μš” | + +--- + +## πŸ“Ž μ°Έκ³  사항 + +- μ†Œμ…œ 둜그인 λ¦¬λ””λ ‰μ…˜ URI도 `https://local.juulabel.com` κΈ°μ€€μœΌλ‘œ 등둝해야 ν•©λ‹ˆλ‹€. +- λΈŒλΌμš°μ €κ°€ μΏ ν‚€λ₯Ό ν—ˆμš©ν•˜λ €λ©΄: + - `Secure: true` + - `SameSite: None` + - 도메인 일치 ν•„μš” diff --git a/src/main/java/com/juu/juulabel/auth/service/AuthService.java b/src/main/java/com/juu/juulabel/auth/service/AuthService.java index c5bf8af..f21d2c3 100644 --- a/src/main/java/com/juu/juulabel/auth/service/AuthService.java +++ b/src/main/java/com/juu/juulabel/auth/service/AuthService.java @@ -99,16 +99,16 @@ public void deleteAccount(Member member, WithdrawalRequest request) { private void handleNewMember(OAuthUser oAuthUser) { String nonce = memberCreationService.createPendingMember(oAuthUser); - signupTokenService.createToken(oAuthUser, nonce); - httpResponseService.redirectToSignup(); + signupTokenService.createAndSetToken(oAuthUser, nonce); + httpResponseService.redirectToSignup(oAuthUser.email()); log.debug("New member flow initiated for: {}", oAuthUser.email()); } private void handlePendingMember(Member member, OAuthUser oAuthUser) { String nonce = memberCreationService.getExistingNonce(member); - signupTokenService.createToken(oAuthUser, nonce); - httpResponseService.redirectToSignup(); + signupTokenService.createAndSetToken(oAuthUser, nonce); + httpResponseService.redirectToSignup(oAuthUser.email()); log.debug("Pending member flow initiated for: {}", oAuthUser.email()); } diff --git a/src/main/java/com/juu/juulabel/auth/service/OAuthLoginService.java b/src/main/java/com/juu/juulabel/auth/service/OAuthLoginService.java index 8e24cef..3b4b627 100644 --- a/src/main/java/com/juu/juulabel/auth/service/OAuthLoginService.java +++ b/src/main/java/com/juu/juulabel/auth/service/OAuthLoginService.java @@ -43,7 +43,8 @@ public OAuthUser authenticateWithProvider(Provider provider, String code) { return providerFactory.getOAuthUser(provider, code, redirectUrl); } catch (Exception e) { - throw new AuthException(ErrorCode.INVALID_AUTHENTICATION); + log.error("OAuth authentication failed", e); + throw new AuthException(ErrorCode.OAUTH_AUTHENTICATION_FAILED); } } diff --git a/src/main/java/com/juu/juulabel/auth/service/SignupTokenService.java b/src/main/java/com/juu/juulabel/auth/service/SignupTokenService.java index 48d0fdd..91021c8 100644 --- a/src/main/java/com/juu/juulabel/auth/service/SignupTokenService.java +++ b/src/main/java/com/juu/juulabel/auth/service/SignupTokenService.java @@ -1,5 +1,6 @@ package com.juu.juulabel.auth.service; +import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -36,6 +37,7 @@ public class SignupTokenService extends PasetoTokenService { private static final String PROVIDER_ID_CLAIM = "providerId"; private static final String NONCE_CLAIM = "nonce"; private static final String AUDIENCE_CLAIM_KEY = "aud"; + private static final Duration SIGN_UP_TOKEN_TTL = Duration.ofMinutes(15); private final SignupTokenValidator validator; private final MemberReader memberReader; @@ -47,7 +49,7 @@ public SignupTokenService( MemberReader memberReader, CookieService cookieService) { - super(secretKey, AuthConstants.SIGN_UP_TOKEN_DURATION); + super(secretKey, SIGN_UP_TOKEN_TTL); this.validator = validator; this.memberReader = memberReader; this.cookieService = cookieService; @@ -68,7 +70,7 @@ public void createAndSetToken(OAuthUser oAuthUser, String nonce) { cookieService.addCookie( AuthConstants.SIGN_UP_TOKEN_NAME, token, - (int) AuthConstants.SIGN_UP_TOKEN_DURATION.toSeconds()); + (int) SIGN_UP_TOKEN_TTL.toSeconds()); } /** @@ -108,15 +110,6 @@ public String resolveToken(String header) { return header.replace(AuthConstants.TOKEN_PREFIX, ""); } - /** - * Creates signup token and sets it as cookie - * This method name maintains compatibility with the existing - * SignupTokenProvider - */ - public void createToken(OAuthUser oAuthUser, String nonce) { - createAndSetToken(oAuthUser, nonce); - } - /** * Converts PASETO Claims to Map for easier processing */ diff --git a/src/main/java/com/juu/juulabel/common/auth/SignupTokenAuthenticationStrategy.java b/src/main/java/com/juu/juulabel/common/auth/SignupTokenAuthenticationStrategy.java index 0cbafa3..7165209 100644 --- a/src/main/java/com/juu/juulabel/common/auth/SignupTokenAuthenticationStrategy.java +++ b/src/main/java/com/juu/juulabel/common/auth/SignupTokenAuthenticationStrategy.java @@ -39,7 +39,7 @@ public Authentication authenticate(HttpServletRequest request) { if (signupToken == null || signupToken.trim().isEmpty()) { log.warn("Signup token missing for signup request: {}", request.getRequestURI()); - throw new AuthException(ErrorCode.SIGN_UP_SESSION_EXPIRED); + throw new AuthException(ErrorCode.SIGN_UP_TOKEN_NOT_FOUND); } try { diff --git a/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java b/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java index b525ebe..fea3edd 100644 --- a/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java +++ b/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java @@ -56,7 +56,8 @@ public class SecurityConfig { "http://localhost:8080", "http://localhost:8084", "http://localhost:5173", - "http://localhost:3000", + "http://localhost:3000", + "https://local.juulabel.com:3000", "https://juulabel.com", "https://api.juulabel.com", "https://dev.juulabel.com", diff --git a/src/main/java/com/juu/juulabel/common/constants/AuthConstants.java b/src/main/java/com/juu/juulabel/common/constants/AuthConstants.java index 751fd4e..8b24ea5 100644 --- a/src/main/java/com/juu/juulabel/common/constants/AuthConstants.java +++ b/src/main/java/com/juu/juulabel/common/constants/AuthConstants.java @@ -1,7 +1,5 @@ package com.juu.juulabel.common.constants; -import java.time.Duration; - import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -12,8 +10,7 @@ public class AuthConstants { public static final String AUTH_TOKEN_NAME = "auth_token"; public static final String SIGN_UP_TOKEN_NAME = "sign_up_token"; - public static final int USER_SESSION_TTL = 60 * 60 * 24 * 7; - public static final Duration SIGN_UP_TOKEN_DURATION = Duration.ofMinutes(15); + public static final int USER_SESSION_TTL = 60 * 60 * 24 * 7; // 7 days // Redis Prefix public static final String USER_SESSION_PREFIX = "user_session"; diff --git a/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java b/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java index 89bdee9..0000f98 100644 --- a/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java +++ b/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java @@ -23,13 +23,6 @@ public enum ErrorCode { INVALID_AUTHENTICATION(HttpStatus.UNAUTHORIZED, "인증이 μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), NOT_FOUND(HttpStatus.NOT_FOUND, "μš”μ²­ν•œ λ¦¬μ†ŒμŠ€λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."), - /** - * CSRF Security - */ - CSRF_TOKEN_INVALID(HttpStatus.FORBIDDEN, "CSRF 토큰이 μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), - CSRF_TOKEN_MISSING(HttpStatus.FORBIDDEN, "CSRF 토큰이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€."), - CSRF_TOKEN_MISMATCH(HttpStatus.FORBIDDEN, "CSRF 토큰이 μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), - /** * Json Web Token */ @@ -53,8 +46,10 @@ public enum ErrorCode { * Authorization */ DEVICE_ID_REQUIRED(HttpStatus.BAD_REQUEST, "헀더에 Device-Idκ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€."), + OAUTH_AUTHENTICATION_FAILED(HttpStatus.BAD_REQUEST, "μ†Œμ…œ 둜그인 인증에 μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€."), OAUTH_PROVIDER_NOT_FOUND(HttpStatus.BAD_REQUEST, "μ†Œμ…œ 둜그인 경둜λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."), + SIGN_UP_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "νšŒμ›κ°€μž… 토큰이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€."), SIGN_UP_SESSION_EXPIRED(HttpStatus.BAD_REQUEST, "νšŒμ›κ°€μž… μ„Έμ…˜μ΄ λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€."), /** @@ -88,6 +83,7 @@ public enum ErrorCode { /** * Alcohol */ + ALCOHOL_TYPE_DUPLICATE(HttpStatus.BAD_REQUEST, "μ€‘λ³΅λœ 주쒅이 μžˆμŠ΅λ‹ˆλ‹€."), ALCOHOL_TYPE_NOT_FOUND(HttpStatus.BAD_REQUEST, "주쒅을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."), ALCOHOLIC_DRINKS_TYPE_NOT_FOUND(HttpStatus.BAD_REQUEST, "전톡주λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."), ALCOHOLIC_DRINKS_INVALID_RATING(HttpStatus.BAD_REQUEST, "잘λͺ»λœ ν‰μ μž…λ‹ˆλ‹€. 평점은 0.00μ—μ„œ 5.00 사이여야 ν•©λ‹ˆλ‹€."), diff --git a/src/main/java/com/juu/juulabel/common/http/CookieService.java b/src/main/java/com/juu/juulabel/common/http/CookieService.java index 1cff164..5df2bbd 100644 --- a/src/main/java/com/juu/juulabel/common/http/CookieService.java +++ b/src/main/java/com/juu/juulabel/common/http/CookieService.java @@ -31,6 +31,7 @@ public class CookieService { /** * Retrieves a cookie value by name from the current HTTP request + * * @param name the cookie name to search for * @return the cookie value if found, null otherwise */ @@ -44,24 +45,26 @@ public Optional getCookie(String name) { /** * Adds a secure HTTP-only cookie to the response with comprehensive security * settings - * @param name the cookie name - * @param value the cookie value + * + * @param name the cookie name + * @param value the cookie value * @param maxAge the cookie max age in seconds */ public void addCookie(String name, String value, int maxAge) { httpContextService.getCurrentResponseOptional().ifPresentOrElse( response -> { - Cookie cookie = createSecureCookie(name, value, maxAge); + Cookie cookie = createCookie(name, value, maxAge); response.addCookie(cookie); + log.debug("Added secure cookie: {} with maxAge: {}", name, maxAge); }, - () -> log.warn("Cannot add cookie '{}' - no HTTP response context available", name) - ); + () -> log.warn("Cannot add cookie '{}' - no HTTP response context available", name)); } /** * Removes a cookie by setting its max age to 0 and clearing its value. * This method ensures proper cookie removal across different browsers. + * * @param name the cookie name to remove */ public void removeCookie(String name) { @@ -69,21 +72,17 @@ public void removeCookie(String name) { response -> { // Create removal cookie with both secure and non-secure variants // to ensure removal regardless of original cookie settings - Cookie removeCookie = createRemovalCookie(name, false); + Cookie removeCookie = createRemovalCookie(name); response.addCookie(removeCookie); - // Also add secure variant for removal - Cookie secureRemoveCookie = createRemovalCookie(name, true); - response.addCookie(secureRemoveCookie); - log.debug("Removed cookie: {}", name); }, - () -> log.warn("Cannot remove cookie '{}' - no HTTP response context available", name) - ); + () -> log.warn("Cannot remove cookie '{}' - no HTTP response context available", name)); } /** * Checks if a cookie with the given name exists in the current request + * * @param name the cookie name to check * @return true if cookie exists, false otherwise */ @@ -93,6 +92,7 @@ public boolean cookieExists(String name) { /** * Gets all cookies from the current request + * * @return array of cookies, or empty array if none exist */ public Cookie[] getAllCookies() { @@ -103,6 +103,7 @@ public Cookie[] getAllCookies() { /** * Validates cookie name according to RFC standards + * * @param name cookie name to validate * @return true if valid cookie name */ @@ -110,7 +111,7 @@ public boolean isValidCookieName(String name) { if (name == null || name.trim().isEmpty()) { return false; } - + // Basic validation - no spaces, control characters, or special chars return name.matches("^[a-zA-Z0-9_-]+$"); } @@ -118,25 +119,16 @@ public boolean isValidCookieName(String name) { /** * Creates a secure cookie with comprehensive security settings */ - private Cookie createSecureCookie(String name, String value, int maxAge) { + private Cookie createCookie(String name, String value, int maxAge) { boolean isSecure = cookieProperties.isSecure(); Cookie cookie = new Cookie(name, value); - // Set domain only for production/secure environments - if (isSecure) { - cookie.setDomain(cookieProperties.getDomain()); - } - + cookie.setDomain(cookieProperties.getDomain()); cookie.setPath(cookieProperties.getPath()); cookie.setHttpOnly(cookieProperties.isHttpOnly()); cookie.setSecure(isSecure); cookie.setMaxAge(maxAge); - - // Set SameSite attribute based on security requirements - String sameSite = isSecure ? - cookieProperties.getSameSiteSecure() : - cookieProperties.getSameSiteNonSecure(); - cookie.setAttribute("SameSite", sameSite); + cookie.setAttribute("SameSite", cookieProperties.getSameSite()); return cookie; } @@ -144,17 +136,16 @@ private Cookie createSecureCookie(String name, String value, int maxAge) { /** * Creates a cookie specifically for removal purposes */ - private Cookie createRemovalCookie(String name, boolean isSecure) { + private Cookie createRemovalCookie(String name) { + boolean isSecure = cookieProperties.isSecure(); Cookie cookie = new Cookie(name, EMPTY_VALUE); - if (isSecure) { - cookie.setDomain(cookieProperties.getDomain()); - cookie.setSecure(true); - } - + cookie.setDomain(cookieProperties.getDomain()); cookie.setPath(cookieProperties.getPath()); cookie.setHttpOnly(cookieProperties.isHttpOnly()); + cookie.setSecure(isSecure); cookie.setMaxAge(COOKIE_REMOVAL_MAX_AGE); + cookie.setAttribute("SameSite", cookieProperties.getSameSite()); return cookie; } @@ -173,4 +164,4 @@ private Optional findCookieByName(Cookie[] cookies, String name) { .map(Cookie::getValue) .findFirst(); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/http/HttpResponseService.java b/src/main/java/com/juu/juulabel/common/http/HttpResponseService.java index 6195d4a..db231a1 100644 --- a/src/main/java/com/juu/juulabel/common/http/HttpResponseService.java +++ b/src/main/java/com/juu/juulabel/common/http/HttpResponseService.java @@ -39,8 +39,8 @@ public void redirectToLogin() { /** * Redirects to the configured signup URL */ - public void redirectToSignup() { - redirect(redirectProperties.getSignupUrl()); + public void redirectToSignup(String email) { + redirect(redirectProperties.getSignupUrl(email)); log.debug("Redirected to signup page"); } diff --git a/src/main/java/com/juu/juulabel/common/http/RequestDataExtractor.java b/src/main/java/com/juu/juulabel/common/http/RequestDataExtractor.java index ebab046..3d694f8 100644 --- a/src/main/java/com/juu/juulabel/common/http/RequestDataExtractor.java +++ b/src/main/java/com/juu/juulabel/common/http/RequestDataExtractor.java @@ -23,11 +23,12 @@ public class RequestDataExtractor { private static final String DEVICE_ID_HEADER_NAME = "Device-Id"; - + private final HttpContextService httpContextService; /** * Checks if the current request path matches the given prefix + * * @param pathPrefix Path prefix to match against * @return true if path matches, false otherwise */ @@ -40,6 +41,7 @@ public boolean isPathMatch(String pathPrefix) { /** * Extracts Authorization header from current request + * * @return Authorization header value, or null if not present */ public Optional getAuthorizationHeader() { @@ -48,6 +50,7 @@ public Optional getAuthorizationHeader() { /** * Extracts User-Agent header from current request + * * @return User-Agent header value, or null if not present */ public Optional getUserAgent() { @@ -56,22 +59,23 @@ public Optional getUserAgent() { /** * Extracts device ID from request headers with fallback to parameter + * * @return Device ID value * @throws BaseException if Device-Id is missing or empty */ public String getDeviceId() { HttpServletRequest request = httpContextService.getCurrentRequest(); - + // Try header first String deviceId = request.getHeader(DEVICE_ID_HEADER_NAME); - + // Fallback to state parameter if (!StringUtils.hasText(deviceId)) { deviceId = request.getParameter("state"); } if (!StringUtils.hasText(deviceId)) { - log.warn("Device-Id not found in headers or parameters for request: {}", + log.warn("Device-Id not found in headers or parameters for request: {}", request.getRequestURI()); throw new BaseException(ErrorCode.DEVICE_ID_REQUIRED); } @@ -79,8 +83,15 @@ public String getDeviceId() { return deviceId.trim(); } + public boolean isLocalRequest() { + HttpServletRequest request = httpContextService.getCurrentRequest(); + String serverName = request.getServerName(); + return serverName.contains("local"); + } + /** * Safely extracts device ID without throwing exception + * * @return Optional containing device ID if present */ public Optional getDeviceIdOptional() { @@ -94,6 +105,7 @@ public Optional getDeviceIdOptional() { /** * Gets a specific header value from the current request + * * @param headerName Name of the header to retrieve * @return Optional containing header value if present */ @@ -105,6 +117,7 @@ public Optional getHeaderValue(String headerName) { /** * Gets a specific parameter value from the current request + * * @param parameterName Name of the parameter to retrieve * @return Optional containing parameter value if present */ @@ -116,6 +129,7 @@ public Optional getParameterValue(String parameterName) { /** * Gets the current request URI + * * @return Optional containing request URI if available */ public Optional getRequestURI() { @@ -125,6 +139,7 @@ public Optional getRequestURI() { /** * Gets the current request method + * * @return Optional containing request method if available */ public Optional getRequestMethod() { @@ -134,10 +149,11 @@ public Optional getRequestMethod() { /** * Gets the remote address from the request + * * @return Optional containing remote address if available */ public Optional getRemoteAddress() { return httpContextService.getCurrentRequestOptional() .map(HttpServletRequest::getRemoteAddr); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/properties/CookieProperties.java b/src/main/java/com/juu/juulabel/common/properties/CookieProperties.java index ad15f2b..3b782e6 100644 --- a/src/main/java/com/juu/juulabel/common/properties/CookieProperties.java +++ b/src/main/java/com/juu/juulabel/common/properties/CookieProperties.java @@ -11,52 +11,57 @@ */ @Data @Component -@ConfigurationProperties(prefix = "app.cookie") +@ConfigurationProperties(prefix = "server.servlet.session.cookie") public class CookieProperties { /** * Whether cookies should be marked as secure (HTTPS only). * Default: false (for development) */ - private boolean secure = false; + private boolean secure; /** * Default domain for cookies. * Default: juulabel.com */ - private String domain = "juulabel.com"; + private String domain; /** * Default path for cookies. - * Default: / + * Default: /app */ - private String path = "/"; + private String path; /** * Default SameSite attribute for secure cookies. * Options: None, Lax, Strict * Default: None (for cross-site requests) */ - private String sameSiteSecure = "None"; - - /** - * Default SameSite attribute for non-secure cookies. - * Options: None, Lax, Strict - * Default: Lax (balanced security and functionality) - */ - private String sameSiteNonSecure = "Lax"; + private String sameSite; /** * Whether to set HttpOnly flag on cookies by default. * Default: true (recommended for security) */ - private boolean httpOnly = true; + private boolean httpOnly; public boolean isSecure() { return secure; } + public String getDomain() { + return domain; + } + + public String getPath() { + return path; + } + public boolean isHttpOnly() { return httpOnly; } + + public String getSameSite() { + return sameSite; + } } diff --git a/src/main/java/com/juu/juulabel/common/properties/RedirectProperties.java b/src/main/java/com/juu/juulabel/common/properties/RedirectProperties.java index 5c26faf..36eb40d 100644 --- a/src/main/java/com/juu/juulabel/common/properties/RedirectProperties.java +++ b/src/main/java/com/juu/juulabel/common/properties/RedirectProperties.java @@ -4,35 +4,57 @@ import org.springframework.stereotype.Component; import lombok.Data; +import lombok.RequiredArgsConstructor; import com.juu.juulabel.member.domain.Provider; +import com.juu.juulabel.common.http.RequestDataExtractor; @Data @Component +@RequiredArgsConstructor @ConfigurationProperties(prefix = "app.redirect") public class RedirectProperties { - private String baseServer; - private String baseClient; + private final RequestDataExtractor requestDataExtractor; + + private String localServer; + private String localClient; + private String remoteServer; + private String remoteClient; private String callback; private String login; private String signup; private String error; public String getRedirectUrl(Provider provider) { - return baseServer + callback + "/" + provider.name().toLowerCase(); + return getContextAwareServerUrl() + callback + "/" + provider.name().toLowerCase(); } public String getLoginUrl() { - return baseClient + login; + return getContextAwareClientUrl() + login; } - public String getSignupUrl() { - return baseClient + signup; + public String getSignupUrl(String email) { + return getContextAwareClientUrl() + signup + "?email=" + email; } public String getErrorUrl() { - return baseClient + error; + return getContextAwareClientUrl() + error; + } + + // Example method using RequestDataExtractor + public String getContextAwareServerUrl() { + if (requestDataExtractor.isLocalRequest()) { + return localServer; + } + return remoteServer; + } + + public String getContextAwareClientUrl() { + if (requestDataExtractor.isLocalRequest()) { + return localClient; + } + return remoteClient; } } diff --git a/src/main/java/com/juu/juulabel/member/util/MemberUtils.java b/src/main/java/com/juu/juulabel/member/util/MemberUtils.java index 91781fb..b9c2957 100644 --- a/src/main/java/com/juu/juulabel/member/util/MemberUtils.java +++ b/src/main/java/com/juu/juulabel/member/util/MemberUtils.java @@ -54,13 +54,11 @@ public void processMemberData(Member member, SignUpMemberRequest signUpRequest) // Process alcohol types if provided if (hasAlcoholTypes(signUpRequest)) { processAlcoholTypes(member, signUpRequest); - } // Process terms agreements if provided if (hasTermsAgreements(signUpRequest)) { processTermsAgreements(member, signUpRequest); - } } catch (InvalidParamException e) { @@ -84,7 +82,7 @@ private void processAlcoholTypes(Member member, SignUpMemberRequest signUpReques .toList(); if (uniqueAlcoholTypeIds.size() != alcoholTypeIds.size()) { - + throw new InvalidParamException(ErrorCode.ALCOHOL_TYPE_DUPLICATE); } List memberAlcoholTypeList = createMemberAlcoholTypeList( @@ -139,35 +137,30 @@ private List validateAndCreateTermsAgreements(Member member, List activeTermsList, List termsAgreements) { Map agreementMap = termsAgreements.stream() .collect(Collectors.toMap(TermsAgreement::termsId, Function.identity())); - // λͺ¨λ“  ν™œμ„± 약관에 λŒ€ν•œ λ™μ˜κ°€ μžˆλŠ”μ§€ 확인 - List missingTermsIds = activeTermsList.stream() + // λͺ¨λ“  ν™œμ„± 약관에 λŒ€ν•œ λ™μ˜κ°€ μžˆλŠ”μ§€ 확인 (μ‘°κΈ° μ’…λ£Œ μ΅œμ ν™”) + boolean hasMissingAgreement = activeTermsList.stream() .map(Terms::getId) - .filter(termsId -> !agreementMap.containsKey(termsId)) - .toList(); - - if (!missingTermsIds.isEmpty()) { + .anyMatch(termsId -> !agreementMap.containsKey(termsId)); + if (hasMissingAgreement) { throw new InvalidParamException(ErrorCode.TERMS_AGREEMENT_MISMATCH); } - // ν•„μˆ˜ μ•½κ΄€ λ™μ˜ 확인 - List requiredTermsNotAgreed = activeTermsList.stream() + // ν•„μˆ˜ μ•½κ΄€ λ™μ˜ 확인 (μ‘°κΈ° μ’…λ£Œ μ΅œμ ν™”) + boolean hasUnagreedRequiredTerms = activeTermsList.stream() .filter(Terms::isRequired) - .filter(terms -> { + .anyMatch(terms -> { TermsAgreement agreement = agreementMap.get(terms.getId()); return agreement == null || !agreement.isAgreed(); - }) - .map(Terms::getId) - .toList(); - - if (!requiredTermsNotAgreed.isEmpty()) { + }); + if (hasUnagreedRequiredTerms) { throw new InvalidParamException(ErrorCode.TERMS_AGREEMENT_MISSING_REQUIRED); } } diff --git a/src/main/java/com/juu/juulabel/redis/UserSessionManager.java b/src/main/java/com/juu/juulabel/redis/UserSessionManager.java index 0c1d527..f271a66 100644 --- a/src/main/java/com/juu/juulabel/redis/UserSessionManager.java +++ b/src/main/java/com/juu/juulabel/redis/UserSessionManager.java @@ -61,6 +61,8 @@ public void createSession(Member member) { sessionService.createSession(session); cookieService.addCookie(AuthConstants.AUTH_TOKEN_NAME, sessionId, AuthConstants.USER_SESSION_TTL); + cookieService.removeCookie(AuthConstants.SIGN_UP_TOKEN_NAME); + log.debug("Session created successfully for member: {}", member.getEmail()); }