v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.

세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-22 11:05:40 +09:00
parent 9bc0f0a90e
commit de767095ba
116 changed files with 8128 additions and 2933 deletions

View File

@@ -130,6 +130,7 @@ const mapAdminMemberRow = (row) => {
* @property {string} avatarUrl - 아바타 URL
* @property {boolean} isAdmin - 관리자 여부
* @property {'owner' | 'admin' | 'vip' | 'member'} role - 권한 코드
* @property {number} sessionVersion - 세션 무효화 버전
* @property {string} createdAt - 생성 시각(ISO)
* @property {string} updatedAt - 수정 시각(ISO)
* @property {string | null} lastSeenAt - 최근 접속 시각(ISO)
@@ -169,6 +170,7 @@ export const getUserByEmail = async (email) => {
avatar_url AS "avatarUrl",
is_admin AS "isAdmin",
user_role AS "role",
session_version AS "sessionVersion",
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
@@ -313,6 +315,7 @@ export const createUser = async (input) => {
avatar_url AS "avatarUrl",
is_admin AS "isAdmin",
user_role AS "role",
session_version AS "sessionVersion",
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
@@ -367,6 +370,7 @@ export const upsertBootstrapOwner = async (input) => {
UPDATE users
SET
password_hash = ${input.passwordHash},
session_version = session_version + 1,
is_admin = true,
user_role = ${MEMBER_ROLE.OWNER},
updated_at = now()
@@ -379,6 +383,7 @@ export const upsertBootstrapOwner = async (input) => {
avatar_url AS "avatarUrl",
is_admin AS "isAdmin",
user_role AS "role",
session_version AS "sessionVersion",
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
@@ -406,6 +411,7 @@ export const upsertBootstrapOwner = async (input) => {
avatar_url AS "avatarUrl",
is_admin AS "isAdmin",
user_role AS "role",
session_version AS "sessionVersion",
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
@@ -480,6 +486,7 @@ export const updateMemberPasswordByEmail = async (input) => {
UPDATE users
SET
password_hash = ${input.passwordHash},
session_version = session_version + 1,
updated_at = now()
WHERE lower(email) = lower(${input.email})
RETURNING id
@@ -499,6 +506,7 @@ export const updateMemberPassword = async (input) => {
UPDATE users
SET
password_hash = ${input.passwordHash},
session_version = session_version + 1,
updated_at = now()
WHERE id = ${input.userId}
`
@@ -787,6 +795,7 @@ export const getAdminUserByEmail = async (email) => {
avatar_url AS "avatarUrl",
is_admin AS "isAdmin",
user_role AS "role",
session_version AS "sessionVersion",
created_at AS "createdAt",
updated_at AS "updatedAt",
last_seen_at AS "lastSeenAt",
@@ -826,16 +835,37 @@ export const getMemberBootstrapState = async () => {
}
/**
* 관리자 권한을 가진 회원 여부를 확인한다.
* 회원 세션 버전이 현재 DB 값과 일치하는지 확인한다.
* @param {string} userId - 사용자 ID
* @returns {Promise<boolean>} 관리자 권한 여부
* @param {number} sessionVersion - 세션 토큰 버전
* @returns {Promise<boolean>} 현재 세션 여부
*/
export const isPrivilegedMember = async (userId) => {
export const isMemberSessionCurrent = async (userId, sessionVersion) => {
const sql = requireSql()
const rows = await sql`
SELECT id
FROM users
WHERE id = ${userId}
AND session_version = ${sessionVersion}
LIMIT 1
`
return Boolean(rows?.[0])
}
/**
* 관리자 권한과 세션 버전을 함께 확인한다.
* @param {string} userId - 사용자 ID
* @param {number} sessionVersion - 세션 토큰 버전
* @returns {Promise<boolean>} 현재 관리자 세션 여부
*/
export const isPrivilegedMemberSession = async (userId, sessionVersion) => {
const sql = requireSql()
const rows = await sql`
SELECT id
FROM users
WHERE id = ${userId}
AND session_version = ${sessionVersion}
AND user_role = ANY(${PRIVILEGED_ROLES})
LIMIT 1
`