mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
fix(session): add auto-refresh for expired JWT tokens
- Add isTokenValid() to decode JWT and check exp claim - Refresh tokens 60 seconds before expiry (buffer) - Auto-fetch fresh token via SSO-Link when cached token expires - Clear invalid sessions when refresh fails - Prevents "exp claim timestamp check failed" errors JWT tokens from mana-core-auth expire after 15 minutes, but sessions were cached for 7 days. Now tokens are transparently refreshed when they expire, keeping users authenticated. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4b950b7083
commit
acf4512e90
6 changed files with 335 additions and 11 deletions
|
|
@ -22,7 +22,14 @@ pnpm type-check # TypeScript check
|
|||
|
||||
## Matrix Commands
|
||||
|
||||
### Analytics (Umami)
|
||||
### Personal Stats (requires login)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `!mystats` | Your personal statistics across all ManaCore apps |
|
||||
| `!status` | Account status and credit balance |
|
||||
|
||||
### Global Analytics (Umami)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
|
|
@ -41,11 +48,12 @@ pnpm type-check # TypeScript check
|
|||
| `!db` | PostgreSQL & Redis status |
|
||||
| `!growth` | User growth statistics |
|
||||
|
||||
### General
|
||||
### Account
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `!status` | Account status |
|
||||
| `!login email password` | Login with ManaCore credentials |
|
||||
| `!logout` | Logout from current session |
|
||||
| `!help` | Show available commands |
|
||||
|
||||
## Scheduled Reports
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { MatrixService } from './matrix.service';
|
|||
import { AnalyticsModule } from '../analytics/analytics.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { InfrastructureModule } from '../infrastructure/infrastructure.module';
|
||||
import { MyDataModule } from '../mydata/mydata.module';
|
||||
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
|
|
@ -10,6 +11,7 @@ import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-
|
|||
AnalyticsModule,
|
||||
UsersModule,
|
||||
InfrastructureModule,
|
||||
MyDataModule,
|
||||
TranscriptionModule.register({
|
||||
sttUrl: process.env.STT_URL || 'http://localhost:3020',
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { InfrastructureService } from '../infrastructure/infrastructure.service';
|
||||
import { MyDataService } from '../mydata/mydata.service';
|
||||
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -28,6 +29,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
{ keywords: ['traffic', 'requests', 'http', 'api'], command: 'traffic' },
|
||||
{ keywords: ['db', 'database', 'datenbank', 'postgres', 'redis'], command: 'db' },
|
||||
{ keywords: ['growth', 'wachstum', 'registrierungen'], command: 'growth' },
|
||||
{ keywords: ['mystats', 'meinestats', 'meinedaten', 'mydata'], command: 'mystats' },
|
||||
]);
|
||||
|
||||
constructor(
|
||||
|
|
@ -35,6 +37,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
private analyticsService: AnalyticsService,
|
||||
private usersService: UsersService,
|
||||
private infrastructureService: InfrastructureService,
|
||||
private myDataService: MyDataService,
|
||||
private readonly transcriptionService: TranscriptionService,
|
||||
private sessionService: SessionService,
|
||||
private creditService: CreditService
|
||||
|
|
@ -146,6 +149,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
await this.sendGrowth(roomId);
|
||||
break;
|
||||
|
||||
case 'mystats':
|
||||
await this.sendMyStats(roomId, sender);
|
||||
break;
|
||||
|
||||
default:
|
||||
await this.sendMessage(roomId, `Unbekannter Befehl: !${command}\n\nVerwende !help`);
|
||||
}
|
||||
|
|
@ -154,7 +161,11 @@ export class MatrixService extends BaseMatrixService {
|
|||
private async sendHelp(roomId: string) {
|
||||
const helpText = `**📊 ManaCore Stats Bot**
|
||||
|
||||
**Analytics (Umami):**
|
||||
**Persönliche Stats:**
|
||||
- \`!mystats\` - Deine persönlichen Statistiken
|
||||
- \`!status\` - Account Status
|
||||
|
||||
**Globale Analytics (Umami):**
|
||||
- \`!stats\` - Übersicht aller Apps (30 Tage)
|
||||
- \`!today\` - Heutige Statistiken
|
||||
- \`!week\` - Wochenstatistiken
|
||||
|
|
@ -168,7 +179,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
- \`!growth\` - User Wachstum
|
||||
|
||||
**Account:**
|
||||
- \`!status\` - Account Status
|
||||
- \`!login email passwort\` - Anmelden
|
||||
- \`!logout\` - Abmelden
|
||||
- \`!help\` - Diese Hilfe`;
|
||||
|
||||
await this.sendMessage(roomId, helpText);
|
||||
|
|
@ -314,6 +326,31 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
}
|
||||
|
||||
private async sendMyStats(roomId: string, sender: string) {
|
||||
const token = await this.sessionService.getToken(sender);
|
||||
|
||||
if (!token) {
|
||||
await this.sendMessage(roomId, this.myDataService.formatNotLoggedIn());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sendMessage(roomId, '📊 Lade deine persönlichen Stats...');
|
||||
const userData = await this.myDataService.getUserData(token);
|
||||
|
||||
if (!userData) {
|
||||
await this.sendMessage(roomId, this.myDataService.formatError());
|
||||
return;
|
||||
}
|
||||
|
||||
const report = this.myDataService.formatUserStats(userData);
|
||||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to fetch user stats:', error);
|
||||
await this.sendMessage(roomId, this.myDataService.formatError());
|
||||
}
|
||||
}
|
||||
|
||||
private async handleStatus(roomId: string, sender: string) {
|
||||
const loggedIn = await this.sessionService.isLoggedIn(sender);
|
||||
const session = await this.sessionService.getSession(sender);
|
||||
|
|
|
|||
10
services/matrix-stats-bot/src/mydata/mydata.module.ts
Normal file
10
services/matrix-stats-bot/src/mydata/mydata.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { MyDataService } from './mydata.service';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [MyDataService],
|
||||
exports: [MyDataService],
|
||||
})
|
||||
export class MyDataModule {}
|
||||
168
services/matrix-stats-bot/src/mydata/mydata.service.ts
Normal file
168
services/matrix-stats-bot/src/mydata/mydata.service.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
interface EntityCount {
|
||||
entity: string;
|
||||
count: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ProjectDataSummary {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
icon: string;
|
||||
available: boolean;
|
||||
error?: string;
|
||||
entities: EntityCount[];
|
||||
totalCount: number;
|
||||
lastActivityAt?: string;
|
||||
}
|
||||
|
||||
interface UserDataSummary {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
createdAt: string;
|
||||
emailVerified: boolean;
|
||||
};
|
||||
auth: {
|
||||
sessionsCount: number;
|
||||
accountsCount: number;
|
||||
has2FA: boolean;
|
||||
lastLoginAt: string | null;
|
||||
};
|
||||
credits: {
|
||||
balance: number;
|
||||
totalEarned: number;
|
||||
totalSpent: number;
|
||||
transactionsCount: number;
|
||||
};
|
||||
projects: ProjectDataSummary[];
|
||||
totals: {
|
||||
totalEntities: number;
|
||||
projectsWithData: number;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MyDataService {
|
||||
private readonly logger = new Logger(MyDataService.name);
|
||||
private readonly authUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.authUrl = this.configService.get<string>('MANA_CORE_AUTH_URL') || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
async getUserData(token: string): Promise<UserDataSummary | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.authUrl}/api/v1/me/data`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.logger.error(`Failed to fetch user data: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await response.json()) as UserDataSummary;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error fetching user data: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
formatUserStats(data: UserDataSummary): string {
|
||||
const formatDate = (dateStr: string): string => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
return num.toLocaleString('de-DE');
|
||||
};
|
||||
|
||||
let report = '**📊 Deine ManaCore Stats**\n\n';
|
||||
|
||||
// Account info
|
||||
report += '**👤 Account**\n';
|
||||
report += `- Email: ${data.user.email}\n`;
|
||||
report += `- Name: ${data.user.name || 'Nicht angegeben'}\n`;
|
||||
report += `- Mitglied seit: ${formatDate(data.user.createdAt)}\n`;
|
||||
report += `- Email verifiziert: ${data.user.emailVerified ? '✅' : '❌'}\n`;
|
||||
if (data.auth.has2FA) {
|
||||
report += `- 2FA: ✅ Aktiv\n`;
|
||||
}
|
||||
report += '\n';
|
||||
|
||||
// Credits
|
||||
report += '**⚡ Credits**\n';
|
||||
report += `- Guthaben: ${data.credits.balance.toFixed(2)}\n`;
|
||||
report += `- Verdient: ${data.credits.totalEarned.toFixed(2)}\n`;
|
||||
report += `- Ausgegeben: ${data.credits.totalSpent.toFixed(2)}\n`;
|
||||
report += `- Transaktionen: ${formatNumber(data.credits.transactionsCount)}\n`;
|
||||
report += '\n';
|
||||
|
||||
// Projects with data
|
||||
const projectsWithData = data.projects.filter((p) => p.available && p.totalCount > 0);
|
||||
|
||||
if (projectsWithData.length > 0) {
|
||||
report += '**📱 Deine Nutzung**\n';
|
||||
|
||||
for (const project of projectsWithData) {
|
||||
const entitySummary = project.entities
|
||||
.filter((e) => e.count > 0)
|
||||
.map((e) => `${formatNumber(e.count)} ${e.label}`)
|
||||
.join(', ');
|
||||
|
||||
report += `${project.icon} **${project.projectName}:** ${entitySummary}\n`;
|
||||
}
|
||||
report += '\n';
|
||||
}
|
||||
|
||||
// Summary
|
||||
report += '**📈 Gesamt**\n';
|
||||
report += `- Datenpunkte: ${formatNumber(data.totals.totalEntities)}\n`;
|
||||
report += `- Aktive Apps: ${data.totals.projectsWithData}/${data.projects.length}\n`;
|
||||
|
||||
// Last activity
|
||||
const lastActivities = projectsWithData
|
||||
.filter((p) => p.lastActivityAt)
|
||||
.map((p) => ({ name: p.projectName, date: new Date(p.lastActivityAt!) }))
|
||||
.sort((a, b) => b.date.getTime() - a.date.getTime());
|
||||
|
||||
if (lastActivities.length > 0) {
|
||||
const latest = lastActivities[0];
|
||||
report += `- Letzte Aktivität: ${latest.name} (${formatDate(latest.date.toISOString())})`;
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
formatNotLoggedIn(): string {
|
||||
return `**❌ Nicht angemeldet**
|
||||
|
||||
Um deine persönlichen Stats zu sehen, melde dich an:
|
||||
|
||||
\`!login deine@email.de deinpasswort\`
|
||||
|
||||
Nach der Anmeldung kannst du mit \`!mystats\` deine Daten abrufen.`;
|
||||
}
|
||||
|
||||
formatError(): string {
|
||||
return `**❌ Fehler beim Laden**
|
||||
|
||||
Deine Stats konnten nicht abgerufen werden. Bitte versuche es später erneut.
|
||||
|
||||
Falls das Problem weiterhin besteht, melde dich neu an:
|
||||
\`!logout\` und dann \`!login\``;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue