mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 23:26:43 +02:00
feat(bots): enable Redis SSO for todo-bot and calendar-bot
- Activate Redis session storage in both bots for cross-bot SSO - Update SessionHelper to async methods for Redis-backed SessionService - Fix async/await issues in todo-bot and calendar-bot matrix.service.ts - Remove unused imports from calendar-api and todo-api services - Add CALENDAR_BACKEND_URL and MANA_CORE_SERVICE_KEY to .env.development Note: SessionService methods are now async (Redis-backed). Other bots need their matrix.service.ts updated to await these async calls. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7bad849258
commit
2777f604fd
27 changed files with 2997 additions and 838 deletions
208
services/mana-core-auth/src/auth/matrix-session.controller.ts
Normal file
208
services/mana-core-auth/src/auth/matrix-session.controller.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
Headers,
|
||||
UnauthorizedException,
|
||||
NotFoundException,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { MatrixSessionService } from './services/matrix-session.service';
|
||||
|
||||
/**
|
||||
* DTO for linking a Matrix user to a Mana account
|
||||
*/
|
||||
class LinkMatrixUserDto {
|
||||
/** Matrix user ID (e.g., @user:matrix.mana.how) */
|
||||
matrixUserId!: string;
|
||||
/** User's email (optional, for convenience) */
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matrix Session Controller
|
||||
*
|
||||
* Provides endpoints for Matrix bot authentication via SSO.
|
||||
*
|
||||
* Endpoints:
|
||||
* - POST /api/v1/auth/matrix-user-links - Link Matrix user to Mana account
|
||||
* - GET /api/v1/auth/matrix-session/:matrixUserId - Get JWT for linked Matrix user
|
||||
* - DELETE /api/v1/auth/matrix-user-links/:matrixUserId - Unlink Matrix user
|
||||
* - GET /api/v1/auth/matrix-user-links/check/:matrixUserId - Check if user is linked
|
||||
*
|
||||
* Authentication:
|
||||
* - POST /link requires Bearer token (user authenticating)
|
||||
* - GET /session requires X-Service-Key (internal bot service)
|
||||
* - DELETE requires Bearer token (user unlinking)
|
||||
* - GET /check requires X-Service-Key (internal bot service)
|
||||
*/
|
||||
@Controller('api/v1/auth')
|
||||
export class MatrixSessionController {
|
||||
constructor(private readonly matrixSessionService: MatrixSessionService) {}
|
||||
|
||||
/**
|
||||
* Link a Matrix user ID to a Mana account
|
||||
*
|
||||
* Called by bots after successful !login command.
|
||||
* Requires the user's JWT token from login.
|
||||
*
|
||||
* @example
|
||||
* POST /api/v1/auth/matrix-user-links
|
||||
* Authorization: Bearer <jwt-token>
|
||||
* Body: { "matrixUserId": "@user:matrix.mana.how", "email": "user@example.com" }
|
||||
*/
|
||||
@Post('matrix-user-links')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async linkMatrixUser(
|
||||
@Body() dto: LinkMatrixUserDto,
|
||||
@Headers('authorization') authHeader?: string,
|
||||
@Headers('x-service-key') serviceKey?: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
// Two auth methods: Bearer token (from user login) or Service key (from bot)
|
||||
let manaUserId: string;
|
||||
|
||||
if (serviceKey && this.matrixSessionService.validateServiceKey(serviceKey)) {
|
||||
// Service key auth - must provide userId in body
|
||||
const bodyWithUserId = dto as LinkMatrixUserDto & { userId?: string };
|
||||
if (!bodyWithUserId.userId) {
|
||||
throw new UnauthorizedException('userId required when using service key');
|
||||
}
|
||||
manaUserId = bodyWithUserId.userId;
|
||||
} else if (authHeader?.startsWith('Bearer ')) {
|
||||
// JWT auth - extract user ID from token
|
||||
const token = authHeader.substring(7);
|
||||
const payload = this.decodeToken(token);
|
||||
if (!payload?.sub) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
manaUserId = payload.sub;
|
||||
} else {
|
||||
throw new UnauthorizedException('Authentication required');
|
||||
}
|
||||
|
||||
if (!dto.matrixUserId) {
|
||||
throw new UnauthorizedException('matrixUserId is required');
|
||||
}
|
||||
|
||||
await this.matrixSessionService.linkMatrixUser(dto.matrixUserId, manaUserId, dto.email);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Matrix user ${dto.matrixUserId} linked successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JWT token for a linked Matrix user
|
||||
*
|
||||
* Called by bots to auto-authenticate users.
|
||||
* Requires service key (internal service authentication).
|
||||
*
|
||||
* @example
|
||||
* GET /api/v1/auth/matrix-session/@user:matrix.mana.how
|
||||
* X-Service-Key: <service-key>
|
||||
*/
|
||||
@Get('matrix-session/:matrixUserId')
|
||||
async getMatrixSession(
|
||||
@Param('matrixUserId') matrixUserId: string,
|
||||
@Headers('x-service-key') serviceKey?: string
|
||||
): Promise<{ token: string; email: string }> {
|
||||
// Require service key for this endpoint
|
||||
if (!serviceKey || !this.matrixSessionService.validateServiceKey(serviceKey)) {
|
||||
throw new UnauthorizedException('Valid service key required');
|
||||
}
|
||||
|
||||
const result = await this.matrixSessionService.getSessionForMatrixUser(
|
||||
decodeURIComponent(matrixUserId)
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new NotFoundException('No link found for this Matrix user');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a Matrix user from a Mana account
|
||||
*
|
||||
* Called when user wants to disconnect their Matrix account.
|
||||
* Requires the user's JWT token.
|
||||
*
|
||||
* @example
|
||||
* DELETE /api/v1/auth/matrix-user-links/@user:matrix.mana.how
|
||||
* Authorization: Bearer <jwt-token>
|
||||
*/
|
||||
@Delete('matrix-user-links/:matrixUserId')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async unlinkMatrixUser(
|
||||
@Param('matrixUserId') matrixUserId: string,
|
||||
@Headers('authorization') authHeader?: string,
|
||||
@Headers('x-service-key') serviceKey?: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
// Allow both Bearer token and service key
|
||||
if (
|
||||
!authHeader?.startsWith('Bearer ') &&
|
||||
!this.matrixSessionService.validateServiceKey(serviceKey || '')
|
||||
) {
|
||||
throw new UnauthorizedException('Authentication required');
|
||||
}
|
||||
|
||||
const deleted = await this.matrixSessionService.unlinkMatrixUser(
|
||||
decodeURIComponent(matrixUserId)
|
||||
);
|
||||
|
||||
if (!deleted) {
|
||||
throw new NotFoundException('No link found for this Matrix user');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Matrix user ${matrixUserId} unlinked successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a Matrix user is linked
|
||||
*
|
||||
* Requires service key (internal service authentication).
|
||||
*
|
||||
* @example
|
||||
* GET /api/v1/auth/matrix-user-links/check/@user:matrix.mana.how
|
||||
* X-Service-Key: <service-key>
|
||||
*/
|
||||
@Get('matrix-user-links/check/:matrixUserId')
|
||||
async checkMatrixLink(
|
||||
@Param('matrixUserId') matrixUserId: string,
|
||||
@Headers('x-service-key') serviceKey?: string
|
||||
): Promise<{ linked: boolean }> {
|
||||
// Require service key for this endpoint
|
||||
if (!serviceKey || !this.matrixSessionService.validateServiceKey(serviceKey)) {
|
||||
throw new UnauthorizedException('Valid service key required');
|
||||
}
|
||||
|
||||
const linked = await this.matrixSessionService.isLinked(decodeURIComponent(matrixUserId));
|
||||
|
||||
return { linked };
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode JWT token to get payload (without verification)
|
||||
* Note: This is used only to extract user ID after the bot has verified the token
|
||||
*/
|
||||
private decodeToken(token: string): { sub?: string } | null {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
|
||||
return JSON.parse(payload);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue