v0.1.62 - 운영 인증 링크 노출 제한
This commit is contained in:
@@ -10,3 +10,4 @@ APP_BASE_URL=https://planner.sori.studio
|
|||||||
RESEND_API_KEY=replace-with-private-resend-api-key
|
RESEND_API_KEY=replace-with-private-resend-api-key
|
||||||
MAIL_FROM_EMAIL=planner@sori.studio
|
MAIL_FROM_EMAIL=planner@sori.studio
|
||||||
MAIL_FROM_NAME=10 Minute Planner
|
MAIL_FROM_NAME=10 Minute Planner
|
||||||
|
AUTH_PREVIEW_LINKS=false
|
||||||
|
|||||||
@@ -222,6 +222,7 @@
|
|||||||
- 왼쪽 사이드바의 인쇄 영역은 `PRINT` 버튼 하나로 줄이고, 클릭 시 모달에서 시작일/종료일과 `1페이지씩` 또는 `2페이지씩` 출력 방식을 선택한다. 인쇄 전용 렌더링은 선택 기간의 날짜를 순서대로 여러 장 생성하며, 2페이지씩 출력에서 홀수 날짜가 남으면 오른쪽은 빈 페이지 프레임으로 둔다.
|
- 왼쪽 사이드바의 인쇄 영역은 `PRINT` 버튼 하나로 줄이고, 클릭 시 모달에서 시작일/종료일과 `1페이지씩` 또는 `2페이지씩` 출력 방식을 선택한다. 인쇄 전용 렌더링은 선택 기간의 날짜를 순서대로 여러 장 생성하며, 2페이지씩 출력에서 홀수 날짜가 남으면 오른쪽은 빈 페이지 프레임으로 둔다.
|
||||||
- 회원가입과 프로필 수정 시 이메일뿐 아니라 닉네임 중복도 서버에서 409로 막는다. 비밀번호 재설정 API는 로그인 모달의 `비밀번호 찾기` 흐름과 연결되어 있고, `/reset-password?token=...` URL로 들어오면 새 비밀번호 설정 모드가 열린다. 실제 메일 발송 전까지는 백엔드 응답의 `resetPreviewUrl`을 개발용 링크로 표시한다.
|
- 회원가입과 프로필 수정 시 이메일뿐 아니라 닉네임 중복도 서버에서 409로 막는다. 비밀번호 재설정 API는 로그인 모달의 `비밀번호 찾기` 흐름과 연결되어 있고, `/reset-password?token=...` URL로 들어오면 새 비밀번호 설정 모드가 열린다. 실제 메일 발송 전까지는 백엔드 응답의 `resetPreviewUrl`을 개발용 링크로 표시한다.
|
||||||
- Resend 메일러가 추가되었다. `RESEND_API_KEY`, `MAIL_FROM_EMAIL`, `MAIL_FROM_NAME`, `APP_BASE_URL` 환경변수를 설정하면 이메일 인증과 비밀번호 재설정 메일을 실제로 발송한다. API 키는 저장소에 커밋하지 말고 루트 `.env`/`.env.dev`에만 넣는다.
|
- Resend 메일러가 추가되었다. `RESEND_API_KEY`, `MAIL_FROM_EMAIL`, `MAIL_FROM_NAME`, `APP_BASE_URL` 환경변수를 설정하면 이메일 인증과 비밀번호 재설정 메일을 실제로 발송한다. API 키는 저장소에 커밋하지 말고 루트 `.env`/`.env.dev`에만 넣는다.
|
||||||
|
- 인증/비밀번호 재설정 개발용 링크는 `AUTH_PREVIEW_LINKS=true`일 때만 API 응답에 포함된다. 운영 `.env`는 반드시 `AUTH_PREVIEW_LINKS=false`로 두어야 하며, 현재 예시 파일도 false가 기본값이다.
|
||||||
- `5173` 포트가 직접 `npm run dev` 없이 열려 있으면 대부분 `ten-minute-frontend-dev` 컨테이너가 떠 있는 상태다. 개발용 Docker를 끄려면 `docker compose -f docker-compose.dev.yml down`을 사용한다.
|
- `5173` 포트가 직접 `npm run dev` 없이 열려 있으면 대부분 `ten-minute-frontend-dev` 컨테이너가 떠 있는 상태다. 개발용 Docker를 끄려면 `docker compose -f docker-compose.dev.yml down`을 사용한다.
|
||||||
- 현재 환경에서는 Docker 데몬이 꺼져 있어서 `docker compose build` 실검증은 하지 못했고, 데몬 시작 후 다시 확인이 필요하다.
|
- 현재 환경에서는 Docker 데몬이 꺼져 있어서 `docker compose build` 실검증은 하지 못했고, 데몬 시작 후 다시 확인이 필요하다.
|
||||||
- 이미지 저장 기능은 추후 `print-only` 또는 별도 export 전용 레이아웃을 기준으로 구현하면 화면/인쇄/공유 결과를 맞추기 쉽다.
|
- 이미지 저장 기능은 추후 `print-only` 또는 별도 export 전용 레이아웃을 기준으로 구현하면 화면/인쇄/공유 결과를 맞추기 쉽다.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const envSchema = z.object({
|
|||||||
RESEND_API_KEY: z.string().optional(),
|
RESEND_API_KEY: z.string().optional(),
|
||||||
MAIL_FROM_EMAIL: z.string().email().default('planner@sori.studio'),
|
MAIL_FROM_EMAIL: z.string().email().default('planner@sori.studio'),
|
||||||
MAIL_FROM_NAME: z.string().default('10 Minute Planner'),
|
MAIL_FROM_NAME: z.string().default('10 Minute Planner'),
|
||||||
|
AUTH_PREVIEW_LINKS: z.coerce.boolean().default(false),
|
||||||
ADMIN_ACCOUNT_ID: z.string().min(1),
|
ADMIN_ACCOUNT_ID: z.string().min(1),
|
||||||
ADMIN_ACCOUNT_PASSWORD: z.string().min(12),
|
ADMIN_ACCOUNT_PASSWORD: z.string().min(12),
|
||||||
ADMIN_ACCOUNT_EMAIL: z.string().email(),
|
ADMIN_ACCOUNT_EMAIL: z.string().email(),
|
||||||
|
|||||||
@@ -109,6 +109,17 @@ function sanitizeUser(user) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withPreviewUrl(payload, key, previewUrl) {
|
||||||
|
if (!env.AUTH_PREVIEW_LINKS) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
[key]: previewUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function findUserByNickname(nickname) {
|
async function findUserByNickname(nickname) {
|
||||||
const [user] = await db
|
const [user] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -177,12 +188,11 @@ export async function registerAuthRoutes(app) {
|
|||||||
linkUrl: verification.previewUrl,
|
linkUrl: verification.previewUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
return reply.code(201).send({
|
return reply.code(201).send(withPreviewUrl({
|
||||||
message: '회원가입이 완료되었습니다.',
|
message: '회원가입이 완료되었습니다.',
|
||||||
token,
|
token,
|
||||||
user: sanitizeUser(user),
|
user: sanitizeUser(user),
|
||||||
verificationPreviewUrl: verification.previewUrl,
|
}, 'verificationPreviewUrl', verification.previewUrl))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/auth/login', async (request, reply) => {
|
app.post('/api/auth/login', async (request, reply) => {
|
||||||
@@ -397,10 +407,9 @@ export async function registerAuthRoutes(app) {
|
|||||||
linkUrl: verification.previewUrl,
|
linkUrl: verification.previewUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return withPreviewUrl({
|
||||||
message: '이메일 인증 링크를 준비했습니다.',
|
message: '이메일 인증 링크를 준비했습니다.',
|
||||||
verificationPreviewUrl: verification.previewUrl,
|
}, 'verificationPreviewUrl', verification.previewUrl)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/auth/verification/confirm', async (request, reply) => {
|
app.post('/api/auth/verification/confirm', async (request, reply) => {
|
||||||
@@ -485,10 +494,9 @@ export async function registerAuthRoutes(app) {
|
|||||||
linkUrl: reset.previewUrl,
|
linkUrl: reset.previewUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return withPreviewUrl({
|
||||||
message: '비밀번호 재설정 링크를 준비했습니다.',
|
message: '비밀번호 재설정 링크를 준비했습니다.',
|
||||||
resetPreviewUrl: reset.previewUrl,
|
}, 'resetPreviewUrl', reset.previewUrl)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/auth/password-reset/confirm', async (request, reply) => {
|
app.post('/api/auth/password-reset/confirm', async (request, reply) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user