- config/season 문서(id/startDate/endDate)로 시즌 경계 정의 — 문서가 없으면 시즌제 비활성(기존 동작 유지)
- endDate 다음 날 dailyArchive에서 maybeSettleSeason 1회 실행: 랭킹 대상 전원의 최종 성적(tierPoints·티어·동점 동일 rank)을 users/{uid}/seasonHistory/{seasonId}에 아카이브 후 tierPoints 0 리셋·rankSnapshot 제거
- 유저별 아카이브+리셋을 같은 배치로 묶고 settledAt 마커는 전원 완료 후 기록 — 부분 실패 재실행 시 기정산 유저 점수를 collectionGroup으로 되읽어 순위 보존(멱등)
- 스트릭은 시즌과 무관하게 유지(이월 이득은 streakBonus 상한이 제한)
- seasonHistory 본인 읽기 전용 rules와 seasonId collectionGroup 인덱스 추가, 정산 시나리오 테스트 신규 작성
129 lines
4.7 KiB
Plaintext
129 lines
4.7 KiB
Plaintext
rules_version='2'
|
|
|
|
// Reward data model:
|
|
// products/{id}, orders/{id}: Admin SDK API only.
|
|
// users/{uid}/wallet/current, pointLedger/{txId}:
|
|
// owner-readable and server-write-only. Order recipient PII is never client-readable.
|
|
|
|
service cloud.firestore {
|
|
match /databases/{database}/documents {
|
|
match /users/{uid} {
|
|
allow read: if request.auth != null && request.auth.uid == uid;
|
|
allow update: if request.auth != null && request.auth.uid == uid &&
|
|
request.resource.data.diff(resource.data)
|
|
.affectedKeys().hasOnly(['fcmToken']) &&
|
|
(!('fcmToken' in request.resource.data) ||
|
|
(request.resource.data.fcmToken is string &&
|
|
request.resource.data.fcmToken.size() <= 4096));
|
|
allow create, delete: if false;
|
|
|
|
match /voteHistory/{date} {
|
|
allow read: if request.auth != null && request.auth.uid == uid;
|
|
allow write: if false;
|
|
}
|
|
|
|
// 시즌 종료 시 서버가 기록하는 최종 성적 아카이브 — 본인 조회 전용.
|
|
match /seasonHistory/{seasonId} {
|
|
allow read: if request.auth != null && request.auth.uid == uid;
|
|
allow write: if false;
|
|
}
|
|
|
|
match /attendance/{month} {
|
|
allow read: if request.auth != null && request.auth.uid == uid;
|
|
allow write: if false;
|
|
}
|
|
|
|
match /pointLedger/{id} {
|
|
allow read: if request.auth != null && request.auth.uid == uid;
|
|
allow write: if false;
|
|
}
|
|
|
|
match /wallet/{id} {
|
|
allow read: if request.auth != null && request.auth.uid == uid;
|
|
allow write: if false;
|
|
}
|
|
|
|
// 배송지 주소록 — 본인 전용 클라이언트 직접 CRUD.
|
|
// 주문 recipient는 API(POST /reward/orders)에 인라인 전달되므로
|
|
// 서버는 이 컬렉션을 읽지 않는다. 문서 id는 클라이언트 uuid.
|
|
match /addresses/{addressId} {
|
|
allow read, delete: if request.auth != null && request.auth.uid == uid;
|
|
allow create, update: if request.auth != null && request.auth.uid == uid &&
|
|
request.resource.data.keys().hasOnly([
|
|
'recipientName', 'phone', 'postalCode', 'address1', 'address2',
|
|
'deliveryMemo', 'isDefault', 'order', 'updatedAt'
|
|
]) &&
|
|
request.resource.data.recipientName is string &&
|
|
request.resource.data.recipientName.size() > 0 &&
|
|
request.resource.data.recipientName.size() <= 50 &&
|
|
request.resource.data.phone is string &&
|
|
request.resource.data.phone.size() <= 30 &&
|
|
request.resource.data.postalCode is string &&
|
|
request.resource.data.postalCode.size() <= 10 &&
|
|
request.resource.data.address1 is string &&
|
|
request.resource.data.address1.size() <= 300 &&
|
|
request.resource.data.address2 is string &&
|
|
request.resource.data.address2.size() <= 300 &&
|
|
request.resource.data.deliveryMemo is string &&
|
|
request.resource.data.deliveryMemo.size() <= 300 &&
|
|
request.resource.data.isDefault is bool &&
|
|
request.resource.data.order is int &&
|
|
request.resource.data.order >= 0 &&
|
|
request.resource.data.updatedAt == request.time;
|
|
}
|
|
|
|
// AI 채팅(짹) — 전부 Admin SDK(서버) 전용. 클라이언트가 직접 읽으면
|
|
// 프롬프트 노출, 직접 쓰면 한도 우회·이력 위조(영속 인젝션)가 가능해진다.
|
|
// 이력 조회도 GET /chat/messages 경유. (ai-chat-tech-design.md §4.6)
|
|
match /chatThreads/{threadId} {
|
|
match /{document=**} {
|
|
allow read, write: if false;
|
|
}
|
|
allow read, write: if false;
|
|
}
|
|
|
|
match /chatQuota/{date} {
|
|
allow read, write: if false;
|
|
}
|
|
|
|
match /chatRequests/{clientMessageId} {
|
|
allow read, write: if false;
|
|
}
|
|
}
|
|
|
|
// 상품·재고 및 주문 PII는 모두 API 전용이다.
|
|
match /products/{productId} {
|
|
allow read, write: if false;
|
|
}
|
|
match /orders/{orderId} { allow read, write: if false; }
|
|
|
|
// 시스템 프롬프트·필터 사전·한도 설정 평문 보관 — 노출 시 §7.5 무력화
|
|
match /config/{doc} {
|
|
allow read, write: if false;
|
|
}
|
|
|
|
// 신고 적재함 — Admin(운영) 전용. 생성도 서버(POST /chat/.../report) 경유
|
|
match /chatReports/{reportId} {
|
|
allow read, write: if false;
|
|
}
|
|
|
|
match /games/{gameId} {
|
|
allow read: if true;
|
|
allow write: if false;
|
|
}
|
|
|
|
match /kboCache/{key} {
|
|
allow read: if true;
|
|
allow write: if false;
|
|
}
|
|
|
|
match /kboLocks/{key} {
|
|
allow read, write: if false;
|
|
}
|
|
|
|
match /{document=**} {
|
|
allow read, write: if false;
|
|
}
|
|
}
|
|
}
|