trading-platform-mcp-auth-v2/src/services/team.service.ts
rckrdmrd a9de3e4331 Migración desde trading-platform/apps/mcp-auth - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:33:07 -06:00

434 lines
12 KiB
TypeScript

/**
* Team Service - Team management operations
*/
import { getPool } from '../config';
import { logger } from '../utils/logger';
import {
TeamMember,
TeamMemberWithUser,
Invitation,
InvitationWithDetails,
AddTeamMemberInput,
UpdateTeamMemberInput,
RemoveTeamMemberInput,
ListTeamMembersInput,
CreateInvitationInput,
CreateInvitationResult,
AcceptInvitationInput,
AcceptInvitationResult,
ResendInvitationInput,
ResendInvitationResult,
RevokeInvitationInput,
ListInvitationsInput,
GetInvitationByTokenInput,
} from '../types/team.types';
class TeamService {
// ================== Team Members ==================
/**
* Add a team member
*/
async addTeamMember(input: AddTeamMemberInput): Promise<TeamMember> {
const pool = getPool();
logger.info('Adding team member', {
tenantId: input.tenantId,
userId: input.userId,
});
const result = await pool.query(
`SELECT * FROM teams.add_team_member($1, $2, $3, $4, NULL, $5, $6)`,
[
input.tenantId,
input.userId,
input.memberRole || 'member',
input.addedBy,
input.department,
input.jobTitle,
]
);
// Get the created member
const memberResult = await pool.query(
`SELECT * FROM teams.team_members
WHERE tenant_id = $1 AND user_id = $2 AND removed_at IS NULL`,
[input.tenantId, input.userId]
);
return this.mapTeamMemberRow(memberResult.rows[0]);
}
/**
* Update a team member
*/
async updateTeamMember(input: UpdateTeamMemberInput): Promise<TeamMember> {
const pool = getPool();
logger.info('Updating team member', {
tenantId: input.tenantId,
userId: input.userId,
});
const updates: string[] = [];
const values: unknown[] = [];
let paramIndex = 1;
if (input.memberRole !== undefined) {
updates.push(`member_role = $${paramIndex++}`);
values.push(input.memberRole);
}
if (input.department !== undefined) {
updates.push(`department = $${paramIndex++}`);
values.push(input.department);
}
if (input.jobTitle !== undefined) {
updates.push(`job_title = $${paramIndex++}`);
values.push(input.jobTitle);
}
if (input.status !== undefined) {
updates.push(`status = $${paramIndex++}`);
values.push(input.status);
}
updates.push(`updated_at = CURRENT_TIMESTAMP`);
values.push(input.tenantId);
values.push(input.userId);
const result = await pool.query(
`UPDATE teams.team_members
SET ${updates.join(', ')}
WHERE tenant_id = $${paramIndex++} AND user_id = $${paramIndex++} AND removed_at IS NULL
RETURNING *`,
values
);
if (result.rowCount === 0) {
throw new Error('Team member not found');
}
return this.mapTeamMemberRow(result.rows[0]);
}
/**
* Remove a team member
*/
async removeTeamMember(input: RemoveTeamMemberInput): Promise<boolean> {
const pool = getPool();
logger.info('Removing team member', {
tenantId: input.tenantId,
userId: input.userId,
});
await pool.query(`SELECT teams.remove_team_member($1, $2, $3)`, [
input.tenantId,
input.userId,
input.removedBy,
]);
return true;
}
/**
* List team members
*/
async listTeamMembers(input: ListTeamMembersInput): Promise<TeamMemberWithUser[]> {
const pool = getPool();
let query = `SELECT * FROM teams.v_team_members WHERE tenant_id = $1`;
const values: unknown[] = [input.tenantId];
let paramIndex = 2;
if (input.status) {
query += ` AND status = $${paramIndex++}`;
values.push(input.status);
}
if (input.memberRole) {
query += ` AND member_role = $${paramIndex++}`;
values.push(input.memberRole);
}
if (input.department) {
query += ` AND department = $${paramIndex++}`;
values.push(input.department);
}
query += ` ORDER BY joined_at DESC`;
const result = await pool.query(query, values);
return result.rows.map((row) => this.mapTeamMemberWithUserRow(row));
}
/**
* Get team member by user ID
*/
async getTeamMember(tenantId: string, userId: string): Promise<TeamMemberWithUser | null> {
const pool = getPool();
const result = await pool.query(
`SELECT * FROM teams.v_team_members WHERE tenant_id = $1 AND user_id = $2`,
[tenantId, userId]
);
if (result.rowCount === 0) {
return null;
}
return this.mapTeamMemberWithUserRow(result.rows[0]);
}
/**
* Get team member count
*/
async getTeamMemberCount(tenantId: string): Promise<number> {
const pool = getPool();
const result = await pool.query(
`SELECT COUNT(*) FROM teams.team_members
WHERE tenant_id = $1 AND removed_at IS NULL AND status = 'active'`,
[tenantId]
);
return parseInt(result.rows[0].count, 10);
}
// ================== Invitations ==================
/**
* Create an invitation
*/
async createInvitation(input: CreateInvitationInput): Promise<CreateInvitationResult> {
const pool = getPool();
logger.info('Creating invitation', {
tenantId: input.tenantId,
email: input.email,
});
const result = await pool.query(
`SELECT * FROM teams.create_invitation($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[
input.tenantId,
input.email,
input.invitedBy,
input.roleId || null,
input.firstName || null,
input.lastName || null,
input.department || null,
input.jobTitle || null,
input.personalMessage || null,
input.expiresInDays || 7,
]
);
return {
invitationId: result.rows[0].invitation_id,
token: result.rows[0].token,
};
}
/**
* Accept an invitation
*/
async acceptInvitation(input: AcceptInvitationInput): Promise<AcceptInvitationResult> {
const pool = getPool();
logger.info('Accepting invitation', { userId: input.userId });
const result = await pool.query(`SELECT * FROM teams.accept_invitation($1, $2)`, [
input.token,
input.userId,
]);
const row = result.rows[0];
return {
success: row.success,
tenantId: row.tenant_id,
roleId: row.role_id,
message: row.message,
};
}
/**
* Resend an invitation
*/
async resendInvitation(input: ResendInvitationInput): Promise<ResendInvitationResult> {
const pool = getPool();
logger.info('Resending invitation', { invitationId: input.invitationId });
const result = await pool.query(`SELECT * FROM teams.resend_invitation($1, $2)`, [
input.invitationId,
input.resentBy,
]);
const row = result.rows[0];
return {
success: row.success,
token: row.token,
message: row.message,
};
}
/**
* Revoke an invitation
*/
async revokeInvitation(input: RevokeInvitationInput): Promise<boolean> {
const pool = getPool();
logger.info('Revoking invitation', { invitationId: input.invitationId });
const result = await pool.query(
`UPDATE teams.invitations
SET status = 'revoked', revoked_at = CURRENT_TIMESTAMP, revoked_by = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $1 AND status = 'pending'`,
[input.invitationId, input.revokedBy]
);
return result.rowCount !== null && result.rowCount > 0;
}
/**
* List invitations
*/
async listInvitations(input: ListInvitationsInput): Promise<InvitationWithDetails[]> {
const pool = getPool();
let query = `SELECT * FROM teams.v_invitations WHERE tenant_id = $1`;
const values: unknown[] = [input.tenantId];
if (input.status) {
query += ` AND status = $2`;
values.push(input.status);
}
query += ` ORDER BY sent_at DESC`;
const result = await pool.query(query, values);
return result.rows.map((row) => this.mapInvitationWithDetailsRow(row));
}
/**
* Get invitation by ID
*/
async getInvitation(invitationId: string): Promise<InvitationWithDetails | null> {
const pool = getPool();
const result = await pool.query(`SELECT * FROM teams.v_invitations WHERE id = $1`, [
invitationId,
]);
if (result.rowCount === 0) {
return null;
}
return this.mapInvitationWithDetailsRow(result.rows[0]);
}
/**
* Get invitation by token (for acceptance page)
*/
async getInvitationByToken(input: GetInvitationByTokenInput): Promise<InvitationWithDetails | null> {
const pool = getPool();
const result = await pool.query(
`SELECT v.* FROM teams.v_invitations v
JOIN teams.invitations i ON v.id = i.id
WHERE i.token_hash = encode(sha256($1::bytea), 'hex')`,
[input.token]
);
if (result.rowCount === 0) {
return null;
}
return this.mapInvitationWithDetailsRow(result.rows[0]);
}
/**
* Expire old invitations (scheduled job)
*/
async expireOldInvitations(): Promise<number> {
const pool = getPool();
const result = await pool.query(`SELECT teams.expire_old_invitations()`);
return result.rows[0].expire_old_invitations;
}
// ================== Row Mappers ==================
private mapTeamMemberRow(row: Record<string, unknown>): TeamMember {
return {
id: row.id as string,
tenantId: row.tenant_id as string,
userId: row.user_id as string,
memberRole: row.member_role as TeamMember['memberRole'],
status: row.status as TeamMember['status'],
department: row.department as string | null,
jobTitle: row.job_title as string | null,
joinedViaInvitation: row.joined_via_invitation as string | null,
joinedAt: new Date(row.joined_at as string),
lastActiveAt: row.last_active_at ? new Date(row.last_active_at as string) : null,
settings: (row.settings as Record<string, unknown>) || {},
createdAt: new Date(row.created_at as string),
updatedAt: new Date(row.updated_at as string),
addedBy: row.added_by as string | null,
removedAt: row.removed_at ? new Date(row.removed_at as string) : null,
removedBy: row.removed_by as string | null,
};
}
private mapTeamMemberWithUserRow(row: Record<string, unknown>): TeamMemberWithUser {
return {
...this.mapTeamMemberRow(row),
userEmail: row.user_email as string,
userDisplayName: row.user_display_name as string | null,
userFirstName: row.user_first_name as string | null,
userLastName: row.user_last_name as string | null,
userAvatarUrl: row.user_avatar_url as string | null,
primaryRoleName: row.primary_role_name as string | null,
};
}
private mapInvitationRow(row: Record<string, unknown>): Invitation {
return {
id: row.id as string,
tenantId: row.tenant_id as string,
email: row.email as string,
firstName: row.first_name as string | null,
lastName: row.last_name as string | null,
roleId: row.role_id as string | null,
department: row.department as string | null,
jobTitle: row.job_title as string | null,
personalMessage: row.personal_message as string | null,
status: row.status as Invitation['status'],
expiresAt: new Date(row.expires_at as string),
sentAt: new Date(row.sent_at as string),
resentCount: row.resent_count as number,
lastResentAt: row.last_resent_at ? new Date(row.last_resent_at as string) : null,
respondedAt: row.responded_at ? new Date(row.responded_at as string) : null,
acceptedUserId: row.accepted_user_id as string | null,
createdAt: new Date(row.created_at as string),
updatedAt: new Date(row.updated_at as string),
invitedBy: row.invited_by as string,
revokedAt: row.revoked_at ? new Date(row.revoked_at as string) : null,
revokedBy: row.revoked_by as string | null,
};
}
private mapInvitationWithDetailsRow(row: Record<string, unknown>): InvitationWithDetails {
return {
...this.mapInvitationRow(row),
tenantName: row.tenant_name as string,
roleName: row.role_name as string | null,
invitedByEmail: row.invited_by_email as string,
invitedByName: row.invited_by_name as string | null,
isExpired: row.is_expired as boolean,
};
}
}
export const teamService = new TeamService();