generated from ttipo/project-base
feat(backend): 앱 새 메일 알림(FCM)과 앱 최소 판 확인을 더한다
앱 첫 판의 새 메일 알림을 서버가 보낸다. 알림 글은 "새 메일 N통"뿐이라 보낸 사람 · 제목 · 메일 id 는 Google 을 거치지 않는다. - 기기 등록: PUT · DELETE /api/push-registrations/current, 봤음 DELETE /api/push-registrations/current/unseen. 앱 토큰만 받고(AppClientGuard — 웹 세션은 AUTH_003), 세션마다 한 줄, 같은 FID 는 한 줄만 둔다 (같은 휴대폰에 다른 직원이 로그인하면 앞 직원의 줄을 지운다). 마이그레이션 AddPushRegistrations. - 새 메일 알아채기: 5단계 엔진 연결을 푸시를 켠 직원에게 붙잡아 둔다(MailEventsService.hold). 웹 실시간 알림과 같은 판정이고, 같은 직원은 30초에 한 번까지, N 은 보낼 때 엔진에서 센다(웹에서 읽은 메일은 빠진다). Stalwart 의 Email/query after 는 그 초를 넣지 않아 어댑터가 한 초 앞을 넘긴다(dev 엔진에서 확인). - 보내기는 새 의존성 없이 FCM HTTP v1 의 fid 대상으로(RS256 JWT · OAuth 토큰 기억 · 다시 하기 · 보내기 직전 다시 보기). - 직원이 그 휴대폰을 건드리지 않았는데 앱 세션이 끝나면(저절로 끝남 · 다른 곳에서 비밀번호 바꿈 등) 그 기기에 "새 메일 알림이 멈췄습니다"를 한 번 보낸다(SESSION_ENDED_NOTICE). - GET /api/app-config(로그인 없음 · no-store) — 최소 판과 알림을 쓸 수 있는지. - 서비스 계정 키 FCM_SERVICE_ACCOUNT_KEY(backend env — 없으면 기동하고 알림만 끈다), 퇴사 마무리에 알림 등록을 더한다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { generateKeyPairSync, type KeyObject } from 'crypto';
|
||||
|
||||
/**
|
||||
* 시험용 FCM 서비스 계정 키 — 프로세스마다 RSA 2048 을 한 번 만들어 쓴다(진짜 키가 아니다 — Google 에 보내지 않는다).
|
||||
* 단위 시험(fcm-*.spec.ts · env.validation.spec.ts)이 env 값(base64 한 줄) · 서명 확인(공개 키)에 쓴다
|
||||
*/
|
||||
export interface TestServiceAccount {
|
||||
/** 키 파일 JSON — 칸을 바꿔 틀린 키를 만든다 */
|
||||
readonly json: Record<string, unknown>;
|
||||
/** env 에 넣는 값 — JSON 을 통째로 base64 한 줄 */
|
||||
readonly base64: string;
|
||||
readonly publicKey: KeyObject;
|
||||
readonly privateKeyPem: string;
|
||||
}
|
||||
|
||||
let pair: { publicKey: KeyObject; privateKeyPem: string } | null = null;
|
||||
|
||||
function keyPair(): { publicKey: KeyObject; privateKeyPem: string } {
|
||||
if (!pair) {
|
||||
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
});
|
||||
pair = {
|
||||
publicKey,
|
||||
privateKeyPem: privateKey
|
||||
.export({ type: 'pkcs8', format: 'pem' })
|
||||
.toString(),
|
||||
};
|
||||
}
|
||||
return pair;
|
||||
}
|
||||
|
||||
/** JSON → env 값 (base64 한 줄) */
|
||||
export function toEnvValue(json: unknown): string {
|
||||
return Buffer.from(JSON.stringify(json)).toString('base64');
|
||||
}
|
||||
|
||||
/** 시험용 키 — `overrides` 로 칸을 바꾸거나(undefined 면 그 칸을 뺀다) 더한다 */
|
||||
export function testServiceAccount(
|
||||
overrides: Record<string, unknown> = {},
|
||||
): TestServiceAccount {
|
||||
const { publicKey, privateKeyPem } = keyPair();
|
||||
const json: Record<string, unknown> = {
|
||||
type: 'service_account',
|
||||
project_id: 'arasystem-push-test',
|
||||
private_key_id: '0123456789abcdef0123456789abcdef01234567',
|
||||
private_key: privateKeyPem,
|
||||
client_email: 'arasystem-push@arasystem-push-test.iam.gserviceaccount.com',
|
||||
client_id: '100000000000000000000',
|
||||
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
...overrides,
|
||||
};
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
if (value === undefined) delete json[key];
|
||||
}
|
||||
return { json, base64: toEnvValue(json), publicKey, privateKeyPem };
|
||||
}
|
||||
Reference in New Issue
Block a user