New global push/ module mirroring mail/ and files/storage/: - PushProvider abstraction; default LogPushProvider (no delivery, logs), PUSH_PROVIDER=fcm switches to FcmPushProvider — Firebase Cloud Messaging HTTP v1, authenticated by a service-account JWT exchanged for an OAuth token (no extra dependency; jsonwebtoken does the signing). Prunes tokens FCM reports as invalid. - DeviceToken model (token + platform, bound to a User or GuestAccount), migration + added to the sync log. - POST /api/push/register + /unregister (any of the three token kinds). - PushService.notifyChannel() resolves a channel's readable audience (DIREKT participants / LT / Gemeinde members + guests / whole KC for broadcast), looks up their device tokens (minus the sender), sends. - ChatService.sendMessage() fires it best-effort after persisting. New env: PUSH_PROVIDER, FCM_PROJECT_ID (default konfi-castle-app), GOOGLE_APPLICATION_CREDENTIALS. Verified against local Postgres: register a token, send a Gemeinde-group chat message from another member -> log-push logs "would push ... to 1 device". Real FCM send needs the service-account JSON. npm test 56. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
import { Module } from '@nestjs/common';
|
|
import { ConfigModule } from '@nestjs/config';
|
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
|
import { existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { PrismaModule } from './prisma/prisma.module';
|
|
import { MailModule } from './mail/mail.module';
|
|
import { PushModule } from './push/push.module';
|
|
import { AuthModule } from './auth/auth.module';
|
|
import { KcModule } from './kc/kc.module';
|
|
import { GemeindeModule } from './gemeinde/gemeinde.module';
|
|
import { TeamerModule } from './teamer/teamer.module';
|
|
import { OnboardingModule } from './onboarding/onboarding.module';
|
|
import { WahlModule } from './wahl/wahl.module';
|
|
import { FilesModule } from './files/files.module';
|
|
import { ChatModule } from './chat/chat.module';
|
|
import { SyncModule } from './sync/sync.module';
|
|
|
|
// Prefer the Flutter web build (single entry point at :3000, incl. the OIDC
|
|
// redirect path /v1/auth/callback via SPA fallback). Falls back to the plain
|
|
// interim client if the Flutter build hasn't been produced yet.
|
|
const flutterWeb = join(__dirname, '..', '..', 'client', 'app', 'build', 'web');
|
|
const interimWeb = join(__dirname, '..', '..', 'client', 'web');
|
|
const webRoot =
|
|
process.env.WEB_CLIENT_DIR ?? (existsSync(flutterWeb) ? flutterWeb : interimWeb);
|
|
|
|
@Module({
|
|
imports: [
|
|
ConfigModule.forRoot({ isGlobal: true }),
|
|
// Static web client; the REST API lives under /api (see main.ts) so it
|
|
// never collides. Unmatched non-file paths fall back to index.html so the
|
|
// client-side router owns routes like /v1/auth/callback.
|
|
ServeStaticModule.forRoot({
|
|
rootPath: webRoot,
|
|
exclude: ['/api*'],
|
|
}),
|
|
PrismaModule,
|
|
MailModule,
|
|
PushModule,
|
|
SyncModule,
|
|
AuthModule,
|
|
KcModule,
|
|
GemeindeModule,
|
|
TeamerModule,
|
|
OnboardingModule,
|
|
WahlModule,
|
|
FilesModule,
|
|
ChatModule,
|
|
],
|
|
})
|
|
export class AppModule {}
|