mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 04:46:41 +02:00
🐛 fix(matrix-web): handle Matrix SSO loginToken callback
Add loginWithLoginToken function to exchange Matrix SSO loginToken for credentials. The app layout now detects the loginToken URL parameter and completes the SSO flow. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9e9db590dc
commit
dc0d425f61
53 changed files with 1550 additions and 230 deletions
|
|
@ -34,6 +34,10 @@
|
|||
"types": "./dist/transcription/index.d.ts",
|
||||
"default": "./dist/transcription/index.js"
|
||||
},
|
||||
"./credit": {
|
||||
"types": "./dist/credit/index.d.ts",
|
||||
"default": "./dist/credit/index.js"
|
||||
},
|
||||
"./nutrition": {
|
||||
"types": "./dist/nutrition/index.d.ts",
|
||||
"default": "./dist/nutrition/index.js"
|
||||
|
|
|
|||
61
packages/bot-services/src/credit/credit.module.ts
Normal file
61
packages/bot-services/src/credit/credit.module.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Module, DynamicModule, Global } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { CreditService } from './credit.service';
|
||||
import { CreditModuleOptions, CREDIT_MODULE_OPTIONS } from './types';
|
||||
|
||||
/**
|
||||
* Shared credit management module for Matrix bots
|
||||
*
|
||||
* Provides CreditService for querying credit balances and formatting
|
||||
* credit-related messages for Matrix chat display.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // With explicit configuration
|
||||
* @Module({
|
||||
* imports: [
|
||||
* CreditModule.register({
|
||||
* authUrl: 'http://mana-core-auth:3001',
|
||||
* })
|
||||
* ]
|
||||
* })
|
||||
*
|
||||
* // With ConfigService (reads from auth.url or MANA_CORE_AUTH_URL)
|
||||
* @Module({
|
||||
* imports: [CreditModule.forRoot()]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
@Global()
|
||||
@Module({})
|
||||
export class CreditModule {
|
||||
/**
|
||||
* Register module with explicit options
|
||||
*/
|
||||
static register(options: CreditModuleOptions = {}): DynamicModule {
|
||||
return {
|
||||
module: CreditModule,
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: CREDIT_MODULE_OPTIONS,
|
||||
useValue: options,
|
||||
},
|
||||
CreditService,
|
||||
],
|
||||
exports: [CreditService],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register module with ConfigService (reads MANA_CORE_AUTH_URL from config)
|
||||
*/
|
||||
static forRoot(): DynamicModule {
|
||||
return {
|
||||
module: CreditModule,
|
||||
imports: [ConfigModule],
|
||||
providers: [CreditService],
|
||||
exports: [CreditService],
|
||||
};
|
||||
}
|
||||
}
|
||||
316
packages/bot-services/src/credit/credit.service.ts
Normal file
316
packages/bot-services/src/credit/credit.service.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
import { Injectable, Inject, Logger, Optional } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
CreditBalance,
|
||||
CreditValidationResult,
|
||||
CreditConsumeResult,
|
||||
CreditModuleOptions,
|
||||
CreditStatusMessage,
|
||||
CreditErrorCode,
|
||||
CREDIT_MODULE_OPTIONS,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Shared credit management service for Matrix bots
|
||||
*
|
||||
* Provides credit balance queries, validation, and formatted messages
|
||||
* for displaying credit information in Matrix chat.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In NestJS module
|
||||
* imports: [CreditModule.register({ authUrl: 'http://mana-core-auth:3001' })]
|
||||
*
|
||||
* // In service/controller
|
||||
* const balance = await creditService.getBalance(token);
|
||||
* const statusMsg = creditService.formatStatusMessage(balance);
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class CreditService {
|
||||
private readonly logger = new Logger(CreditService.name);
|
||||
private readonly authUrl: string;
|
||||
private readonly serviceKey?: string;
|
||||
private readonly appId?: string;
|
||||
|
||||
constructor(
|
||||
@Optional() private configService: ConfigService,
|
||||
@Optional() @Inject(CREDIT_MODULE_OPTIONS) private options?: CreditModuleOptions
|
||||
) {
|
||||
// Priority: module options > config > environment > default
|
||||
this.authUrl =
|
||||
options?.authUrl ||
|
||||
this.configService?.get<string>('auth.url') ||
|
||||
this.configService?.get<string>('MANA_CORE_AUTH_URL') ||
|
||||
'http://localhost:3001';
|
||||
|
||||
this.serviceKey =
|
||||
options?.serviceKey || this.configService?.get<string>('MANA_CORE_SERVICE_KEY');
|
||||
|
||||
this.appId = options?.appId || this.configService?.get<string>('APP_ID');
|
||||
|
||||
this.logger.log(`Credit service initialized with auth URL: ${this.authUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credit balance for a user
|
||||
*
|
||||
* @param token - User's JWT token
|
||||
* @returns Credit balance information
|
||||
*/
|
||||
async getBalance(token: string): Promise<CreditBalance> {
|
||||
try {
|
||||
const response = await fetch(`${this.authUrl}/api/v1/credits/balance`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.logger.warn(`Failed to get credit balance: ${response.status}`);
|
||||
return { balance: 0, hasCredits: false };
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { balance?: number; tier?: string };
|
||||
const balance = data.balance ?? 0;
|
||||
|
||||
return {
|
||||
balance,
|
||||
hasCredits: balance > 0,
|
||||
tier: data.tier,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting credit balance:', error);
|
||||
return { balance: 0, hasCredits: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if user has enough credits for an operation
|
||||
*
|
||||
* @param token - User's JWT token
|
||||
* @param requiredCredits - Credits required for the operation
|
||||
* @returns Validation result
|
||||
*/
|
||||
async validateCredits(token: string, requiredCredits: number): Promise<CreditValidationResult> {
|
||||
const balance = await this.getBalance(token);
|
||||
|
||||
return {
|
||||
hasCredits: balance.balance >= requiredCredits,
|
||||
availableCredits: balance.balance,
|
||||
requiredCredits,
|
||||
error:
|
||||
balance.balance < requiredCredits
|
||||
? `Nicht genug Credits. Benötigt: ${requiredCredits}, Vorhanden: ${balance.balance.toFixed(2)}`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format credit balance as a status message for Matrix
|
||||
*
|
||||
* @param balance - Credit balance or number
|
||||
* @returns Formatted message with text and HTML versions
|
||||
*/
|
||||
formatBalanceMessage(balance: CreditBalance | number): CreditStatusMessage {
|
||||
const credits = typeof balance === 'number' ? balance : balance.balance;
|
||||
const hasCredits = credits > 0;
|
||||
|
||||
const icon = hasCredits ? '⚡' : '⚠️';
|
||||
const creditsFormatted = credits.toFixed(2);
|
||||
|
||||
const text = `${icon} Credits: ${creditsFormatted}`;
|
||||
const html = `${icon} <b>Credits:</b> ${creditsFormatted}`;
|
||||
|
||||
return { text, html };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a full status message with credit information
|
||||
*
|
||||
* @param email - User's email (logged in as)
|
||||
* @param balance - Credit balance
|
||||
* @param additionalInfo - Additional status info
|
||||
* @returns Formatted status message
|
||||
*/
|
||||
formatStatusMessage(
|
||||
email: string,
|
||||
balance: CreditBalance,
|
||||
additionalInfo?: Record<string, string>
|
||||
): CreditStatusMessage {
|
||||
const lines: string[] = [];
|
||||
const htmlLines: string[] = [];
|
||||
|
||||
// Header
|
||||
lines.push('🤖 Bot Status');
|
||||
htmlLines.push('<b>🤖 Bot Status</b>');
|
||||
|
||||
// User info
|
||||
lines.push(`👤 User: ${email}`);
|
||||
htmlLines.push(`👤 <b>User:</b> ${email}`);
|
||||
|
||||
// Credits
|
||||
const creditIcon = balance.hasCredits ? '⚡' : '⚠️';
|
||||
const creditsFormatted = balance.balance.toFixed(2);
|
||||
lines.push(`${creditIcon} Credits: ${creditsFormatted}`);
|
||||
htmlLines.push(`${creditIcon} <b>Credits:</b> ${creditsFormatted}`);
|
||||
|
||||
// Tier if available
|
||||
if (balance.tier) {
|
||||
lines.push(`📊 Tier: ${balance.tier}`);
|
||||
htmlLines.push(`📊 <b>Tier:</b> ${balance.tier}`);
|
||||
}
|
||||
|
||||
// Additional info
|
||||
if (additionalInfo) {
|
||||
for (const [key, value] of Object.entries(additionalInfo)) {
|
||||
lines.push(`${key}: ${value}`);
|
||||
htmlLines.push(`<b>${key}:</b> ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Low credits warning
|
||||
if (balance.balance < 10 && balance.balance > 0) {
|
||||
lines.push('');
|
||||
lines.push('⚠️ Nur noch wenig Credits!');
|
||||
lines.push('👉 Credits kaufen: https://mana.how/credits');
|
||||
htmlLines.push('<br>');
|
||||
htmlLines.push('⚠️ <b>Nur noch wenig Credits!</b>');
|
||||
htmlLines.push('👉 <a href="https://mana.how/credits">Credits kaufen</a>');
|
||||
}
|
||||
|
||||
// No credits warning
|
||||
if (!balance.hasCredits) {
|
||||
lines.push('');
|
||||
lines.push('❌ Keine Credits mehr!');
|
||||
lines.push('👉 Credits kaufen: https://mana.how/credits');
|
||||
htmlLines.push('<br>');
|
||||
htmlLines.push('❌ <b>Keine Credits mehr!</b>');
|
||||
htmlLines.push('👉 <a href="https://mana.how/credits">Credits kaufen</a>');
|
||||
}
|
||||
|
||||
return {
|
||||
text: lines.join('\n'),
|
||||
html: htmlLines.join('<br>'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an error message for insufficient credits
|
||||
*
|
||||
* @param required - Required credits
|
||||
* @param available - Available credits
|
||||
* @param operation - Operation name (optional)
|
||||
* @returns Formatted error message
|
||||
*/
|
||||
formatInsufficientCreditsError(
|
||||
required: number,
|
||||
available: number,
|
||||
operation?: string
|
||||
): CreditStatusMessage {
|
||||
const lines: string[] = [];
|
||||
const htmlLines: string[] = [];
|
||||
|
||||
lines.push('❌ Nicht genug Credits');
|
||||
htmlLines.push('❌ <b>Nicht genug Credits</b>');
|
||||
|
||||
if (operation) {
|
||||
lines.push(`Operation: ${operation}`);
|
||||
htmlLines.push(`<b>Operation:</b> ${operation}`);
|
||||
}
|
||||
|
||||
lines.push(`Benötigt: ${required.toFixed(2)} Credits`);
|
||||
lines.push(`Vorhanden: ${available.toFixed(2)} Credits`);
|
||||
htmlLines.push(`<b>Benötigt:</b> ${required.toFixed(2)} Credits`);
|
||||
htmlLines.push(`<b>Vorhanden:</b> ${available.toFixed(2)} Credits`);
|
||||
|
||||
lines.push('');
|
||||
lines.push('👉 Credits kaufen: https://mana.how/credits');
|
||||
htmlLines.push('<br>');
|
||||
htmlLines.push('👉 <a href="https://mana.how/credits">Credits kaufen</a>');
|
||||
|
||||
return {
|
||||
text: lines.join('\n'),
|
||||
html: htmlLines.join('<br>'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a success message after credit consumption
|
||||
*
|
||||
* @param consumed - Credits consumed
|
||||
* @param remaining - Remaining credits
|
||||
* @param operation - Operation description (optional)
|
||||
* @returns Formatted success message
|
||||
*/
|
||||
formatCreditConsumedMessage(
|
||||
consumed: number,
|
||||
remaining: number,
|
||||
operation?: string
|
||||
): CreditStatusMessage {
|
||||
const text = operation
|
||||
? `✅ ${operation}\n⚡ -${consumed.toFixed(2)} Credits (${remaining.toFixed(2)} verbleibend)`
|
||||
: `⚡ -${consumed.toFixed(2)} Credits (${remaining.toFixed(2)} verbleibend)`;
|
||||
|
||||
const html = operation
|
||||
? `✅ ${operation}<br>⚡ -${consumed.toFixed(2)} Credits (${remaining.toFixed(2)} verbleibend)`
|
||||
: `⚡ -${consumed.toFixed(2)} Credits (${remaining.toFixed(2)} verbleibend)`;
|
||||
|
||||
return { text, html };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error code from HTTP status
|
||||
*
|
||||
* @param status - HTTP status code
|
||||
* @returns Credit error code
|
||||
*/
|
||||
getErrorCodeFromStatus(status: number): CreditErrorCode {
|
||||
switch (status) {
|
||||
case 401:
|
||||
return CreditErrorCode.NOT_LOGGED_IN;
|
||||
case 402:
|
||||
return CreditErrorCode.INSUFFICIENT_CREDITS;
|
||||
case 400:
|
||||
return CreditErrorCode.INVALID_OPERATION;
|
||||
default:
|
||||
return CreditErrorCode.SERVICE_UNAVAILABLE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a generic credit error message
|
||||
*
|
||||
* @param errorCode - Credit error code
|
||||
* @returns Formatted error message
|
||||
*/
|
||||
formatErrorMessage(errorCode: CreditErrorCode): CreditStatusMessage {
|
||||
let text: string;
|
||||
let html: string;
|
||||
|
||||
switch (errorCode) {
|
||||
case CreditErrorCode.INSUFFICIENT_CREDITS:
|
||||
text = '❌ Nicht genug Credits\n👉 Credits kaufen: https://mana.how/credits';
|
||||
html =
|
||||
'❌ <b>Nicht genug Credits</b><br>👉 <a href="https://mana.how/credits">Credits kaufen</a>';
|
||||
break;
|
||||
case CreditErrorCode.NOT_LOGGED_IN:
|
||||
text = '❌ Bitte zuerst einloggen: !login email passwort';
|
||||
html = '❌ <b>Bitte zuerst einloggen:</b> <code>!login email passwort</code>';
|
||||
break;
|
||||
case CreditErrorCode.INVALID_OPERATION:
|
||||
text = '❌ Ungültige Operation';
|
||||
html = '❌ <b>Ungültige Operation</b>';
|
||||
break;
|
||||
case CreditErrorCode.SERVICE_UNAVAILABLE:
|
||||
default:
|
||||
text = '❌ Service temporär nicht verfügbar. Bitte später erneut versuchen.';
|
||||
html = '❌ <b>Service temporär nicht verfügbar.</b> Bitte später erneut versuchen.';
|
||||
break;
|
||||
}
|
||||
|
||||
return { text, html };
|
||||
}
|
||||
}
|
||||
10
packages/bot-services/src/credit/index.ts
Normal file
10
packages/bot-services/src/credit/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export { CreditService } from './credit.service';
|
||||
export { CreditModule } from './credit.module';
|
||||
export type {
|
||||
CreditBalance,
|
||||
CreditValidationResult,
|
||||
CreditConsumeResult,
|
||||
CreditModuleOptions,
|
||||
CreditStatusMessage,
|
||||
} from './types';
|
||||
export { CREDIT_MODULE_OPTIONS, CreditErrorCode } from './types';
|
||||
75
packages/bot-services/src/credit/types.ts
Normal file
75
packages/bot-services/src/credit/types.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Types for credit management in Matrix bots
|
||||
*/
|
||||
|
||||
/**
|
||||
* User credit balance information
|
||||
*/
|
||||
export interface CreditBalance {
|
||||
/** Current credit balance */
|
||||
balance: number;
|
||||
/** Whether user has enough credits for basic operations */
|
||||
hasCredits: boolean;
|
||||
/** User's tier (if applicable) */
|
||||
tier?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a credit validation check
|
||||
*/
|
||||
export interface CreditValidationResult {
|
||||
/** Whether user has enough credits */
|
||||
hasCredits: boolean;
|
||||
/** Available credits */
|
||||
availableCredits: number;
|
||||
/** Required credits for the operation */
|
||||
requiredCredits: number;
|
||||
/** Error message if not enough credits */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a credit consumption operation
|
||||
*/
|
||||
export interface CreditConsumeResult {
|
||||
/** Whether credits were successfully consumed */
|
||||
success: boolean;
|
||||
/** New balance after consumption */
|
||||
newBalance?: number;
|
||||
/** Error message if failed */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit module configuration options
|
||||
*/
|
||||
export interface CreditModuleOptions {
|
||||
/** Mana Core Auth URL */
|
||||
authUrl?: string;
|
||||
/** Service key for credit operations */
|
||||
serviceKey?: string;
|
||||
/** App ID for credit operations */
|
||||
appId?: string;
|
||||
}
|
||||
|
||||
export const CREDIT_MODULE_OPTIONS = 'CREDIT_MODULE_OPTIONS';
|
||||
|
||||
/**
|
||||
* Credit error codes for structured error handling
|
||||
*/
|
||||
export enum CreditErrorCode {
|
||||
INSUFFICIENT_CREDITS = 'INSUFFICIENT_CREDITS',
|
||||
NOT_LOGGED_IN = 'NOT_LOGGED_IN',
|
||||
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
|
||||
INVALID_OPERATION = 'INVALID_OPERATION',
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted credit message for Matrix bots
|
||||
*/
|
||||
export interface CreditStatusMessage {
|
||||
/** Plain text message */
|
||||
text: string;
|
||||
/** HTML formatted message (for Matrix) */
|
||||
html: string;
|
||||
}
|
||||
|
|
@ -101,6 +101,16 @@ export type {
|
|||
TranscriptionModuleOptions,
|
||||
} from './transcription/index.js';
|
||||
|
||||
// Credit (Credit balance and formatting for Matrix bots)
|
||||
export { CreditModule, CreditService, CREDIT_MODULE_OPTIONS, CreditErrorCode } from './credit/index.js';
|
||||
export type {
|
||||
CreditBalance,
|
||||
CreditValidationResult,
|
||||
CreditConsumeResult,
|
||||
CreditModuleOptions,
|
||||
CreditStatusMessage,
|
||||
} from './credit/index.js';
|
||||
|
||||
// ===== Placeholder Services (to be implemented) =====
|
||||
|
||||
export { NutritionModule } from './nutrition/index.js';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue