Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/api/src/app/alerts/common/alert-notifier.registry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { SupportedLanguage } from '../../../common/utils/language.util';
import { TelegramService } from '../../telegram/telegram.service';
import { TelegramKeyboardBuilderService } from '../../telegram/telegram-keyboard-builder.service';
import { AlertEventSeverity, AlertEventType } from '../../../entities/alert-event.entity';
Expand All @@ -15,7 +16,7 @@ export interface AlertNotifierPayload {

type TelegramNotifier = (
payload: AlertNotifierPayload,
userLanguage: 'en' | 'fr'
userLanguage: SupportedLanguage
) => Promise<void>;

@Injectable()
Expand All @@ -32,7 +33,7 @@ export class AlertNotifierRegistry {
]);
}

public async notify(payload: AlertNotifierPayload, userLanguage: 'en' | 'fr'): Promise<void> {
public async notify(payload: AlertNotifierPayload, userLanguage: SupportedLanguage): Promise<void> {
const notifier = this.notifiers.get(payload.type);

if (!notifier) {
Expand All @@ -49,12 +50,12 @@ export class AlertNotifierRegistry {
};
}

private async notifySentry(payload: AlertNotifierPayload, userLanguage: 'en' | 'fr'): Promise<void> {
private async notifySentry(payload: AlertNotifierPayload, userLanguage: SupportedLanguage): Promise<void> {
const keyboard = this.keyboardBuilder.buildSentryAlertKeyboard(payload.userId, userLanguage);
await this.telegramService.sendSentryAlert(payload.userId, this.buildAlertInfo(payload), userLanguage, keyboard, false);
}

private async notifyBreakIn(payload: AlertNotifierPayload, userLanguage: 'en' | 'fr'): Promise<void> {
private async notifyBreakIn(payload: AlertNotifierPayload, userLanguage: SupportedLanguage): Promise<void> {
const keyboard = this.keyboardBuilder.buildBreakInAlertKeyboard(payload.userId, userLanguage);
await this.telegramService.sendBreakInAlert(payload.userId, this.buildAlertInfo(payload), userLanguage, keyboard, false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AlertEventSeverity, AlertEventType } from '../../../entities/alert-even
import { AlertsService } from '../alerts.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationQueueService } from '../../notifications/notification-queue.service';
import { SupportedLanguage } from '../../../common/utils/language.util';
import { AlertNotifierPayload, AlertNotifierRegistry } from './alert-notifier.registry';
import { NOTIFICATION_SWEEP_MAX_ATTEMPTS } from '../../../config/notification-sweep-cron.config';

Expand Down Expand Up @@ -183,7 +184,7 @@ export class VehicleAlertNotifierService {
}
}

private async deliverNotifications(payload: AlertNotifierPayload, userLanguage: 'en' | 'fr'): Promise<void> {
private async deliverNotifications(payload: AlertNotifierPayload, userLanguage: SupportedLanguage): Promise<void> {
const [pushResult, telegramResult] = await Promise.allSettled([
this.notificationsService.sendPushAlert(payload.userId, payload.severity, payload.type, userLanguage, payload.correlationId),
this.sendTelegramNotification(payload, userLanguage),
Expand All @@ -197,7 +198,7 @@ export class VehicleAlertNotifierService {
}
}

private async sendTelegramNotification(payload: AlertNotifierPayload, userLanguage: 'en' | 'fr'): Promise<boolean> {
private async sendTelegramNotification(payload: AlertNotifierPayload, userLanguage: SupportedLanguage): Promise<boolean> {
if (!(await this.notificationsService.shouldSendTelegram(payload.userId, payload.severity))) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SupportedLanguage } from '../../../common/utils/language.util';

export const oauthProviderRequirementsSymbol = Symbol('OAuthProviderRequirements');

export interface OAuthUserProfile {
Expand All @@ -16,13 +18,13 @@ export interface OAuthAuthenticationResult {
mobileRedirectUri?: string;
tokens: OAuthTokensResponse;
profile: OAuthUserProfile;
userLocale: 'en' | 'fr';
userLocale: SupportedLanguage;
}

export interface OAuthProviderRequirements {
generateLoginUrl(userLocale: 'en' | 'fr', mobileRedirectUri?: string): { url: string; state: string };
generateLoginUrl(userLocale: SupportedLanguage, mobileRedirectUri?: string): { url: string; state: string };
generateScopeChangeUrl(
userLocale: 'en' | 'fr',
userLocale: SupportedLanguage,
missingScopes?: string[],
mobileRedirectUri?: string
): { url: string; state: string };
Expand Down
15 changes: 9 additions & 6 deletions apps/api/src/app/auth/services/tesla-oauth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import * as crypto from 'crypto';
import * as https from 'https';
import { decode } from 'jsonwebtoken';
import { TeslaScopes } from '@sentryguard/beta-domain';
import { normalizeTeslaLocale } from '../../../common/utils/language.util';
import {
normalizeTeslaLocale,
SupportedLanguage,
} from '../../../common/utils/language.util';
import { MissingPermissionsException } from '../../../common/exceptions/missing-permissions.exception';
import {
OAuthProviderRequirements,
Expand All @@ -22,7 +25,7 @@ import {
interface StatePayload {
mobileRedirectUri?: string;
type: 'oauth_state';
userLocale: 'en' | 'fr';
userLocale: SupportedLanguage;
nonce: string;
}

Expand Down Expand Up @@ -62,7 +65,7 @@ export class TeslaOAuthService implements OAuthProviderRequirements, OnModuleIni
}

generateLoginUrl(
userLocale: 'en' | 'fr' = 'en',
userLocale: SupportedLanguage = 'en',
mobileRedirectUri?: string
): { url: string; state: string } {
const clientId = process.env.TESLA_CLIENT_ID;
Expand Down Expand Up @@ -90,7 +93,7 @@ export class TeslaOAuthService implements OAuthProviderRequirements, OnModuleIni
}

generateScopeChangeUrl(
userLocale: 'en' | 'fr' = 'en',
userLocale: SupportedLanguage = 'en',
missingScopes?: TeslaScopes[],
mobileRedirectUri?: string
): { url: string; state: string } {
Expand Down Expand Up @@ -149,7 +152,7 @@ export class TeslaOAuthService implements OAuthProviderRequirements, OnModuleIni
}
}

private validateOAuthState(state: string): { mobileRedirectUri?: string; userLocale: 'en' | 'fr' } {
private validateOAuthState(state: string): { mobileRedirectUri?: string; userLocale: SupportedLanguage } {
try {
const secret = process.env.JWT_OAUTH_STATE_SECRET;

Expand Down Expand Up @@ -268,7 +271,7 @@ export class TeslaOAuthService implements OAuthProviderRequirements, OnModuleIni
}
}

private createSignedState(userLocale: 'en' | 'fr', mobileRedirectUri?: string): string {
private createSignedState(userLocale: SupportedLanguage, mobileRedirectUri?: string): string {
const payload: StatePayload = {
mobileRedirectUri: this.resolveMobileRedirectUri(mobileRedirectUri),
type: 'oauth_state',
Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/app/auth/services/user-registration.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Repository } from 'typeorm';
import * as crypto from 'crypto';
import { User } from '../../../entities/user.entity';
import { encrypt } from '../../../common/utils/crypto.util';
import { SupportedLanguage } from '../../../common/utils/language.util';
import { UserNotApprovedException } from '../../../common/exceptions/user-not-approved.exception';
import type { WaitlistServiceRequirements } from '../../waitlist/interfaces/waitlist-service.requirements';
import { waitlistServiceRequirementsSymbol } from '../../waitlist/interfaces/waitlist-service.requirements';
Expand All @@ -27,7 +28,7 @@ export class UserRegistrationService {
async createOrUpdateUser(
tokens: OAuthTokensResponse,
profile: OAuthUserProfile,
userLocale: 'en' | 'fr'
userLocale: SupportedLanguage
): Promise<string> {
const encryptedAccessToken = encrypt(tokens.access_token);
const encryptedRefreshToken = encrypt(tokens.refresh_token);
Expand Down Expand Up @@ -61,7 +62,7 @@ export class UserRegistrationService {

private async verifyWaitlistApproval(
profile: OAuthUserProfile,
userLocale: 'en' | 'fr'
userLocale: SupportedLanguage
): Promise<void> {
if (!profile.email) {
return;
Expand Down Expand Up @@ -114,7 +115,7 @@ export class UserRegistrationService {
profile: OAuthUserProfile,
encryptedAccessToken: string,
encryptedRefreshToken: string,
userLocale: 'en' | 'fr'
userLocale: SupportedLanguage
): Promise<string> {
const userId = crypto.randomBytes(16).toString('hex');

Expand Down
13 changes: 7 additions & 6 deletions apps/api/src/app/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { NotificationPreferences } from '../../entities/notification-preferences
import { PushDeviceToken } from '../../entities/push-device-token.entity';
import { AlertEventSeverity, AlertEventType } from '../../entities/alert-event.entity';
import i18n from '../../i18n';
import { SupportedLanguage } from '../../common/utils/language.util';
import { NOTIFICATION_REQUEST_TIMEOUT_MS } from '../../config/notification-timeout.config';
import { withTimeout } from '../../common/utils/with-timeout.util';

Expand Down Expand Up @@ -94,7 +95,7 @@ export class NotificationsService {
userId: string,
severity: AlertEventSeverity,
type: AlertEventType,
userLanguage: 'en' | 'fr',
userLanguage: SupportedLanguage,
correlationId?: string
): Promise<boolean> {
const eligibleDevices = await this.findEligibleDevices(userId, severity);
Expand All @@ -120,7 +121,7 @@ export class NotificationsService {
severity: AlertEventSeverity,
type: AlertEventType,
userId: string,
userLanguage: 'en' | 'fr',
userLanguage: SupportedLanguage,
correlationId?: string
): Promise<void> {
const { body, title } = this.resolveAlertTexts(type, userLanguage);
Expand All @@ -138,7 +139,7 @@ export class NotificationsService {
}
}

private resolveAlertTexts(type: AlertEventType, lng: 'en' | 'fr'): { body: string; title: string } {
private resolveAlertTexts(type: AlertEventType, lng: SupportedLanguage): { body: string; title: string } {
if (type === AlertEventType.BreakIn) {
return {
body: i18n.t('A break-in attempt was detected.', { lng }),
Expand Down Expand Up @@ -232,7 +233,7 @@ export class NotificationsService {
type: AlertEventType,
criticalAlertsEnabled: boolean,
userId: string,
userLanguage: 'en' | 'fr',
userLanguage: SupportedLanguage,
correlationId?: string
): Promise<void> {
try {
Expand Down Expand Up @@ -266,7 +267,7 @@ export class NotificationsService {
type: AlertEventType,
criticalAlertsEnabled: boolean,
userId: string,
userLanguage: 'en' | 'fr'
userLanguage: SupportedLanguage
): object {
const isPriorityAlert = criticalAlertsEnabled && this.shouldUsePriorityChannel(severity, type);
const channelId = isPriorityAlert ? 'sentryguard-critical-alerts-v5' : 'sentryguard-alerts';
Expand All @@ -293,7 +294,7 @@ export class NotificationsService {
return severity === AlertEventSeverity.Critical || type === AlertEventType.Sentry;
}

private buildTeslaRedirectUrl(userId: string, userLanguage: 'en' | 'fr'): string {
private buildTeslaRedirectUrl(userId: string, userLanguage: SupportedLanguage): string {
const baseUrl = process.env.TELEGRAM_WEBHOOK_BASE || 'http://localhost:3000';
return `${baseUrl}/redirect/tesla-app?userId=${encodeURIComponent(userId)}&lang=${userLanguage}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as crypto from 'crypto';
import { SupportedLanguage } from '../../common/utils/language.util';
import { Context } from 'telegraf';
import i18n from '../../i18n';
import { TelegramConfig, TelegramLinkStatus } from '../../entities/telegram-config.entity';
Expand Down Expand Up @@ -87,13 +88,13 @@ export class TelegramAccountLinkingService implements OnModuleInit {
});
}

private async getLanguageForConfig(config: TelegramConfig | null): Promise<'en' | 'fr'> {
private async getLanguageForConfig(config: TelegramConfig | null): Promise<SupportedLanguage> {
return config
? await this.userLanguageService.getUserLanguage(config.userId)
: 'en';
}

private async handleExpiredToken(ctx: Context, config: TelegramConfig, lng: 'en' | 'fr'): Promise<boolean> {
private async handleExpiredToken(ctx: Context, config: TelegramConfig, lng: SupportedLanguage): Promise<boolean> {
if (config.expires_at && new Date() > config.expires_at) {
config.status = TelegramLinkStatus.EXPIRED;
await this.telegramConfigRepository.save(config);
Expand All @@ -103,7 +104,7 @@ export class TelegramAccountLinkingService implements OnModuleInit {
return false;
}

private async linkAccountToChat(ctx: Context, config: TelegramConfig, lng: 'en' | 'fr'): Promise<void> {
private async linkAccountToChat(ctx: Context, config: TelegramConfig, lng: SupportedLanguage): Promise<void> {
const chatId = ctx.chat?.id?.toString();
if (!chatId) {
this.logger.warn('⚠️ chatId missing in Telegram update');
Expand Down Expand Up @@ -132,7 +133,7 @@ export class TelegramAccountLinkingService implements OnModuleInit {
this.logger.log(`✅ Account linked: userId=${config.userId}, chatId=${chatId}`);
}

private async sendLinkSuccessMessages(ctx: Context, lng: 'en' | 'fr', isSetupComplete: boolean): Promise<void> {
private async sendLinkSuccessMessages(ctx: Context, lng: SupportedLanguage, isSetupComplete: boolean): Promise<void> {
const mainMenuKeyboard = this.keyboardBuilderService.buildMainMenuKeyboard(lng);

if (isSetupComplete) {
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/app/telegram/telegram-bot-update.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import i18n from '../../i18n';
import { SupportedLanguage } from '../../common/utils/language.util';
import { TelegramConfig, TelegramLinkStatus } from '../../entities/telegram-config.entity';
import { TelegramBotService } from './telegram-bot.service';
import { TelegramKeyboardBuilderService } from './telegram-keyboard-builder.service';
Expand All @@ -18,7 +19,7 @@ export class TelegramBotUpdateService {
private readonly keyboardBuilderService: TelegramKeyboardBuilderService,
) {}

async ensureUserIsUpToDate(userId: string, chatId: string, lng: 'en' | 'fr'): Promise<void> {
async ensureUserIsUpToDate(userId: string, chatId: string, lng: SupportedLanguage): Promise<void> {
const config = await this.telegramConfigRepository.findOne({
where: { userId, status: TelegramLinkStatus.LINKED },
});
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/app/telegram/telegram-context.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TelegramConfig, TelegramLinkStatus } from '../../entities/telegram-config.entity';
import { UserLanguageService } from '../user/user-language.service';
import { SupportedLanguage } from '../../common/utils/language.util';

@Injectable()
export class TelegramContextService {
Expand All @@ -22,7 +23,7 @@ export class TelegramContextService {
return config?.chat_id ?? null;
}

async getUserLanguageFromChatId(chatId: string): Promise<'en' | 'fr'> {
async getUserLanguageFromChatId(chatId: string): Promise<SupportedLanguage> {
try {
const config = await this.telegramConfigRepository.findOne({
where: { chat_id: chatId, status: TelegramLinkStatus.LINKED },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Injectable } from '@nestjs/common';
import i18n from '../../i18n';
import { SupportedLanguage } from '../../common/utils/language.util';
import { TelegramMessageOptions } from './telegram.types';

@Injectable()
export class TelegramKeyboardBuilderService {

buildSentryAlertKeyboard(
userId: string,
userLanguage: 'en' | 'fr'
userLanguage: SupportedLanguage
) {
const baseUrl = process.env.TELEGRAM_WEBHOOK_BASE || 'http://localhost:3000';
const redirectUrl = `${baseUrl}/redirect/tesla-app?userId=${userId}&lang=${userLanguage}`;
Expand All @@ -26,12 +27,12 @@ export class TelegramKeyboardBuilderService {

buildBreakInAlertKeyboard(
userId: string,
userLanguage: 'en' | 'fr'
userLanguage: SupportedLanguage
) {
return this.buildSentryAlertKeyboard(userId, userLanguage);
}

buildMainMenuKeyboard(lng: 'en' | 'fr', mutedUntil: Date | null | undefined = null): TelegramMessageOptions {
buildMainMenuKeyboard(lng: SupportedLanguage, mutedUntil: Date | null | undefined = null): TelegramMessageOptions {
const isMuted = mutedUntil != null && new Date() < mutedUntil;
const muteButtonKey = isMuted ? 'menuButtonMuteActive' : 'menuButtonMute';

Expand All @@ -46,7 +47,7 @@ export class TelegramKeyboardBuilderService {
};
}

buildMuteActiveKeyboard(lng: 'en' | 'fr'): TelegramMessageOptions {
buildMuteActiveKeyboard(lng: SupportedLanguage): TelegramMessageOptions {
return {
keyboard: {
inline_keyboard: [
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/app/telegram/telegram-mute.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Context } from 'telegraf';
import i18n from '../../i18n';
import { SupportedLanguage } from '../../common/utils/language.util';
import { TelegramConfig, TelegramLinkStatus } from '../../entities/telegram-config.entity';
import { TelegramBotService } from './telegram-bot.service';
import { TelegramKeyboardBuilderService } from './telegram-keyboard-builder.service';
Expand Down Expand Up @@ -143,7 +144,7 @@ export class TelegramMuteService implements OnModuleInit {
);
}

private async confirmMute(ctx: Context, mutedUntil: Date, lng: 'en' | 'fr'): Promise<void> {
private async confirmMute(ctx: Context, mutedUntil: Date, lng: SupportedLanguage): Promise<void> {
const confirmation = i18n.t('muteConfirmed', { lng, duration: TelegramMessageHelper.formatRemainingTime(mutedUntil) });
await ctx.answerCbQuery();
await ctx.deleteMessage();
Expand Down
Loading
Loading