Files
arasystem/backend/test/fcm-test-key.ts
T
ttipo c0edff10d1 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>
2026-11-08 00:49:18 +09:00

59 lines
2.1 KiB
TypeScript

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 };
}