🐛 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:
Till-JS 2026-02-01 15:02:47 +01:00
parent 9e9db590dc
commit dc0d425f61
53 changed files with 1550 additions and 230 deletions

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { ChatModule } from '../chat/chat.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -8,7 +8,7 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { ChatService, Conversation } from '../chat/chat.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService, CreditErrorCode } from '@manacore/bot-services';
import { HELP_MESSAGE, BRANCH_ICONS } from '../config/configuration';
@Injectable()
@ -27,7 +27,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private readonly transcriptionService: TranscriptionService,
private chatService: ChatService,
private sessionService: SessionService
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -136,7 +137,7 @@ export class MatrixService extends BaseMatrixService {
break;
case 'status':
response = this.handleStatus(sender);
response = await this.handleStatus(sender);
break;
case 'chat':
@ -248,21 +249,38 @@ export class MatrixService extends BaseMatrixService {
return 'Erfolgreich abgemeldet.';
}
private handleStatus(sender: string): string {
private async handleStatus(sender: string): Promise<string> {
const isLoggedIn = this.sessionService.isLoggedIn(sender);
const email = this.sessionService.getEmail(sender);
const token = this.sessionService.getToken(sender);
const currentConv = this.getCurrentConversation(sender);
const selectedModel = this.getSelectedModel(sender);
let status = `**Bot Status**\n`;
status += `- Angemeldet: ${isLoggedIn ? 'Ja' : 'Nein'}\n`;
// Get credit balance if logged in
let creditBalance = { balance: 0, hasCredits: false };
if (token) {
creditBalance = await this.creditService.getBalance(token);
}
const additionalInfo: Record<string, string> = {};
if (currentConv) {
status += `- Aktuelles Gespraech: ${currentConv.substring(0, 8)}...\n`;
additionalInfo['🗨️ Gespraech'] = `${currentConv.substring(0, 8)}...`;
}
if (selectedModel) {
status += `- Gewaehltes Modell: ${selectedModel.substring(0, 8)}...\n`;
additionalInfo['🧠 Modell'] = `${selectedModel.substring(0, 8)}...`;
}
status += `- Aktive Sessions: ${this.sessionService.getSessionCount()}`;
return status;
if (!isLoggedIn) {
return `🤖 **Bot Status**\n\n❌ Nicht angemeldet.\n\nNutze \`!login email passwort\` zum Anmelden.`;
}
const statusMessage = this.creditService.formatStatusMessage(
email || 'Unbekannt',
creditBalance,
additionalInfo
);
return statusMessage.text;
}
// Quick chat (stateless)
@ -292,6 +310,16 @@ export class MatrixService extends BaseMatrixService {
);
if (result.error) {
// Handle 402 Payment Required (insufficient credits)
if (result.statusCode === 402) {
const balance = await this.creditService.getBalance(token);
const errorMsg = this.creditService.formatInsufficientCreditsError(
2, // AI Chat costs ~2 credits
balance.balance,
'AI Chat'
);
return errorMsg.text;
}
return `Fehler: ${result.error}`;
}
@ -465,6 +493,16 @@ Nutze \`!senden [nachricht]\` um zu chatten oder \`!verlauf\` fuer den Nachricht
// Get AI response
const completionResult = await this.chatService.createCompletion(token, messages, convResult.data!.modelId);
if (completionResult.error) {
// Handle 402 Payment Required (insufficient credits)
if (completionResult.statusCode === 402) {
const balance = await this.creditService.getBalance(token);
const errorMsg = this.creditService.formatInsufficientCreditsError(
2, // AI Chat costs ~2 credits
balance.balance,
'AI Chat'
);
return errorMsg.text;
}
return `Fehler bei AI-Antwort: ${completionResult.error}`;
}

View file

@ -59,7 +59,7 @@ export class ChatService {
path: string,
token: string,
options: RequestInit = {}
): Promise<{ data?: T; error?: string }> {
): Promise<{ data?: T; error?: string; statusCode?: number }> {
try {
const response = await fetch(this.getUrl(path), {
...options,
@ -72,7 +72,10 @@ export class ChatService {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return { error: errorData.message || `HTTP ${response.status}` };
return {
error: errorData.message || `HTTP ${response.status}`,
statusCode: response.status,
};
}
const data = await response.json();
@ -118,7 +121,7 @@ export class ChatService {
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
modelId: string,
options?: { temperature?: number; maxTokens?: number }
): Promise<{ data?: ChatCompletionResponse; error?: string }> {
): Promise<{ data?: ChatCompletionResponse; error?: string; statusCode?: number }> {
return this.request<ChatCompletionResponse>('/chat/completions', token, {
method: 'POST',
body: JSON.stringify({