add: admin cli
This commit is contained in:
162
admin/src/commands/clear-cache.ts
Normal file
162
admin/src/commands/clear-cache.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import {
|
||||
db,
|
||||
getOrganizationAccess,
|
||||
getOrganizationByProjectIdCached,
|
||||
getProjectAccess,
|
||||
getProjectByIdCached,
|
||||
} from '@openpanel/db';
|
||||
import chalk from 'chalk';
|
||||
import fuzzy from 'fuzzy';
|
||||
import inquirer from 'inquirer';
|
||||
import autocomplete from 'inquirer-autocomplete-prompt';
|
||||
|
||||
// Register autocomplete prompt
|
||||
inquirer.registerPrompt('autocomplete', autocomplete);
|
||||
|
||||
interface OrgSearchItem {
|
||||
id: string;
|
||||
name: string;
|
||||
displayText: string;
|
||||
}
|
||||
|
||||
export async function clearCache() {
|
||||
console.log(chalk.blue('\n🗑️ Clear Cache\n'));
|
||||
console.log('Loading organizations...\n');
|
||||
|
||||
const organizations = await db.organization.findMany({
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (organizations.length === 0) {
|
||||
console.log(chalk.red('No organizations found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const searchItems: OrgSearchItem[] = organizations.map((org) => ({
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
displayText: `${org.name} ${chalk.gray(`(${org.id})`)}`,
|
||||
}));
|
||||
|
||||
const searchFunction = async (_answers: unknown, input = '') => {
|
||||
const fuzzyResult = fuzzy.filter(input, searchItems, {
|
||||
extract: (item: OrgSearchItem) => `${item.name} ${item.id}`,
|
||||
});
|
||||
|
||||
return fuzzyResult.map((result: fuzzy.FilterResult<OrgSearchItem>) => ({
|
||||
name: result.original.displayText,
|
||||
value: result.original,
|
||||
}));
|
||||
};
|
||||
|
||||
const { selectedOrg } = (await inquirer.prompt([
|
||||
{
|
||||
type: 'autocomplete',
|
||||
name: 'selectedOrg',
|
||||
message: 'Search for an organization:',
|
||||
source: searchFunction,
|
||||
pageSize: 15,
|
||||
},
|
||||
])) as { selectedOrg: OrgSearchItem };
|
||||
|
||||
// Fetch organization with all projects
|
||||
const organization = await db.organization.findUnique({
|
||||
where: {
|
||||
id: selectedOrg.id,
|
||||
},
|
||||
include: {
|
||||
projects: {
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
},
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
console.log(chalk.red('Organization not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.yellow('\n📋 Organization Details:\n'));
|
||||
console.log(` ${chalk.gray('ID:')} ${organization.id}`);
|
||||
console.log(` ${chalk.gray('Name:')} ${organization.name}`);
|
||||
console.log(` ${chalk.gray('Projects:')} ${organization.projects.length}`);
|
||||
|
||||
if (organization.projects.length > 0) {
|
||||
console.log(chalk.yellow('\n📊 Projects:\n'));
|
||||
for (const project of organization.projects) {
|
||||
console.log(
|
||||
` - ${project.name} ${chalk.gray(`(${project.id})`)} - ${chalk.cyan(`${project.eventsCount.toLocaleString()} events`)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm before clearing cache
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `Clear cache for organization "${organization.name}" and all ${organization.projects.length} projects?`,
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!confirm) {
|
||||
console.log(chalk.yellow('\nCache clear cancelled.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.blue('\n🔄 Clearing cache...\n'));
|
||||
|
||||
for (const project of organization.projects) {
|
||||
// Clear project access cache for each member
|
||||
for (const member of organization.members) {
|
||||
if (!member.user?.id) continue;
|
||||
console.log(
|
||||
`Clearing cache for project: ${project.name} and member: ${member.user?.email}`,
|
||||
);
|
||||
await getProjectAccess.clear({
|
||||
userId: member.user?.id,
|
||||
projectId: project.id,
|
||||
});
|
||||
await getOrganizationAccess.clear({
|
||||
userId: member.user?.id,
|
||||
organizationId: organization.id,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Clearing cache for project: ${project.name}`);
|
||||
await getOrganizationByProjectIdCached.clear(project.id);
|
||||
await getProjectByIdCached.clear(project.id);
|
||||
}
|
||||
|
||||
console.log(chalk.gray(`Organization ID: ${organization.id}`));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`Project IDs: ${organization.projects.map((p) => p.id).join(', ')}`,
|
||||
),
|
||||
);
|
||||
|
||||
// Example of what you might do:
|
||||
/*
|
||||
for (const project of organization.projects) {
|
||||
console.log(`Clearing cache for project: ${project.name}...`);
|
||||
// await clearProjectCache(project.id);
|
||||
// await redis.del(`project:${project.id}:*`);
|
||||
}
|
||||
|
||||
// Clear organization-level cache
|
||||
// await clearOrganizationCache(organization.id);
|
||||
// await redis.del(`organization:${organization.id}:*`);
|
||||
|
||||
console.log(chalk.green('\n✅ Cache cleared successfully!'));
|
||||
*/
|
||||
}
|
||||
215
admin/src/commands/delete-organization.ts
Normal file
215
admin/src/commands/delete-organization.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
db,
|
||||
deleteFromClickhouse,
|
||||
deleteOrganization as deleteOrg,
|
||||
} from '@openpanel/db';
|
||||
import chalk from 'chalk';
|
||||
import fuzzy from 'fuzzy';
|
||||
import inquirer from 'inquirer';
|
||||
import autocomplete from 'inquirer-autocomplete-prompt';
|
||||
|
||||
// Register autocomplete prompt
|
||||
inquirer.registerPrompt('autocomplete', autocomplete);
|
||||
|
||||
interface OrgSearchItem {
|
||||
id: string;
|
||||
name: string;
|
||||
displayText: string;
|
||||
}
|
||||
|
||||
export async function deleteOrganization() {
|
||||
console.log(chalk.red('\n🗑️ Delete Organization\n'));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ WARNING: This will permanently delete the organization and all its data!\n',
|
||||
),
|
||||
);
|
||||
console.log('Loading organizations...\n');
|
||||
|
||||
const organizations = await db.organization.findMany({
|
||||
include: {
|
||||
projects: true,
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (organizations.length === 0) {
|
||||
console.log(chalk.red('No organizations found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const searchItems: OrgSearchItem[] = organizations.map((org) => ({
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
displayText: `${org.name} ${chalk.gray(`(${org.id})`)} ${chalk.cyan(`- ${org.projects.length} projects, ${org.members.length} members`)}`,
|
||||
}));
|
||||
|
||||
const searchFunction = async (_answers: unknown, input = '') => {
|
||||
const fuzzyResult = fuzzy.filter(input, searchItems, {
|
||||
extract: (item: OrgSearchItem) => `${item.name} ${item.id}`,
|
||||
});
|
||||
|
||||
return fuzzyResult.map((result: fuzzy.FilterResult<OrgSearchItem>) => ({
|
||||
name: result.original.displayText,
|
||||
value: result.original,
|
||||
}));
|
||||
};
|
||||
|
||||
const { selectedOrg } = (await inquirer.prompt([
|
||||
{
|
||||
type: 'autocomplete',
|
||||
name: 'selectedOrg',
|
||||
message: 'Search for an organization to delete:',
|
||||
source: searchFunction,
|
||||
pageSize: 15,
|
||||
},
|
||||
])) as { selectedOrg: OrgSearchItem };
|
||||
|
||||
// Fetch full organization details
|
||||
const organization = await db.organization.findUnique({
|
||||
where: {
|
||||
id: selectedOrg.id,
|
||||
},
|
||||
include: {
|
||||
projects: {
|
||||
include: {
|
||||
clients: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
console.log(chalk.red('Organization not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Display what will be deleted
|
||||
console.log(chalk.red('\n⚠️ YOU ARE ABOUT TO DELETE:\n'));
|
||||
console.log(` ${chalk.bold('Organization:')} ${organization.name}`);
|
||||
console.log(` ${chalk.gray('ID:')} ${organization.id}`);
|
||||
console.log(` ${chalk.gray('Projects:')} ${organization.projects.length}`);
|
||||
console.log(` ${chalk.gray('Members:')} ${organization.members.length}`);
|
||||
|
||||
if (organization.projects.length > 0) {
|
||||
console.log(chalk.red('\n Projects that will be deleted:'));
|
||||
for (const project of organization.projects) {
|
||||
console.log(
|
||||
` - ${project.name} ${chalk.gray(`(${project.eventsCount.toLocaleString()} events, ${project.clients.length} clients)`)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (organization.members.length > 0) {
|
||||
console.log(chalk.red('\n Members who will lose access:'));
|
||||
for (const member of organization.members) {
|
||||
const email = member.user?.email || member.email || 'Unknown';
|
||||
console.log(` - ${email} ${chalk.gray(`(${member.role})`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red(
|
||||
'\n⚠️ This will delete ALL projects, clients, events, and data associated with this organization!',
|
||||
),
|
||||
);
|
||||
|
||||
// First confirmation
|
||||
const { confirmFirst } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirmFirst',
|
||||
message: chalk.red(
|
||||
`Are you ABSOLUTELY SURE you want to delete "${organization.name}"?`,
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!confirmFirst) {
|
||||
console.log(chalk.yellow('\nDeletion cancelled.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Second confirmation - type organization name
|
||||
const { confirmName } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'confirmName',
|
||||
message: `Type the organization name "${organization.name}" to confirm deletion:`,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirmName !== organization.name) {
|
||||
console.log(
|
||||
chalk.red('\n❌ Organization name does not match. Deletion cancelled.'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Final confirmation
|
||||
const { confirmFinal } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirmFinal',
|
||||
message: chalk.red(
|
||||
'FINAL WARNING: This action CANNOT be undone. Delete now?',
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!confirmFinal) {
|
||||
console.log(chalk.yellow('\nDeletion cancelled.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.red('\n🗑️ Deleting organization...\n'));
|
||||
|
||||
try {
|
||||
const projectIds = organization.projects.map((p) => p.id);
|
||||
|
||||
// Step 1: Delete from ClickHouse (events, profiles, etc.)
|
||||
if (projectIds.length > 0) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Deleting data from ClickHouse for ${projectIds.length} projects...`,
|
||||
),
|
||||
);
|
||||
await deleteFromClickhouse(projectIds);
|
||||
console.log(chalk.green('✓ ClickHouse data deletion initiated'));
|
||||
}
|
||||
|
||||
// Step 2: Delete the organization from PostgreSQL (cascade will handle related records)
|
||||
console.log(chalk.yellow('Deleting organization from database...'));
|
||||
await deleteOrg(organization.id);
|
||||
console.log(chalk.green('✓ Organization deleted from database'));
|
||||
|
||||
console.log(chalk.green('\n✅ Organization deleted successfully!'));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`Deleted: ${organization.name} with ${organization.projects.length} projects and ${organization.members.length} members`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
'\nNote: ClickHouse deletions are processed asynchronously and may take a few moments to complete.',
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red('\n❌ Error deleting organization:'), error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
220
admin/src/commands/delete-user.ts
Normal file
220
admin/src/commands/delete-user.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { db } from '@openpanel/db';
|
||||
import chalk from 'chalk';
|
||||
import fuzzy from 'fuzzy';
|
||||
import inquirer from 'inquirer';
|
||||
import autocomplete from 'inquirer-autocomplete-prompt';
|
||||
|
||||
// Register autocomplete prompt
|
||||
inquirer.registerPrompt('autocomplete', autocomplete);
|
||||
|
||||
interface UserSearchItem {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
displayText: string;
|
||||
}
|
||||
|
||||
export async function deleteUser() {
|
||||
console.log(chalk.red('\n🗑️ Delete User\n'));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ WARNING: This will permanently delete the user and remove them from all organizations!\n',
|
||||
),
|
||||
);
|
||||
console.log('Loading users...\n');
|
||||
|
||||
const users = await db.user.findMany({
|
||||
include: {
|
||||
membership: {
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
accounts: true,
|
||||
},
|
||||
orderBy: {
|
||||
email: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (users.length === 0) {
|
||||
console.log(chalk.red('No users found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const searchItems: UserSearchItem[] = users.map((user) => {
|
||||
const fullName =
|
||||
user.firstName || user.lastName
|
||||
? `${user.firstName || ''} ${user.lastName || ''}`.trim()
|
||||
: '';
|
||||
const orgCount = user.membership.length;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
displayText: `${user.email} ${fullName ? chalk.gray(`(${fullName})`) : ''} ${chalk.cyan(`- ${orgCount} orgs`)}`,
|
||||
};
|
||||
});
|
||||
|
||||
const searchFunction = async (_answers: unknown, input = '') => {
|
||||
const fuzzyResult = fuzzy.filter(input, searchItems, {
|
||||
extract: (item: UserSearchItem) =>
|
||||
`${item.email} ${item.firstName || ''} ${item.lastName || ''}`,
|
||||
});
|
||||
|
||||
return fuzzyResult.map((result: fuzzy.FilterResult<UserSearchItem>) => ({
|
||||
name: result.original.displayText,
|
||||
value: result.original,
|
||||
}));
|
||||
};
|
||||
|
||||
const { selectedUser } = (await inquirer.prompt([
|
||||
{
|
||||
type: 'autocomplete',
|
||||
name: 'selectedUser',
|
||||
message: 'Search for a user to delete:',
|
||||
source: searchFunction,
|
||||
pageSize: 15,
|
||||
},
|
||||
])) as { selectedUser: UserSearchItem };
|
||||
|
||||
// Fetch full user details
|
||||
const user = await db.user.findUnique({
|
||||
where: {
|
||||
id: selectedUser.id,
|
||||
},
|
||||
include: {
|
||||
membership: {
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
accounts: true,
|
||||
createdOrganizations: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.log(chalk.red('User not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Display what will be deleted
|
||||
console.log(chalk.red('\n⚠️ YOU ARE ABOUT TO DELETE:\n'));
|
||||
console.log(` ${chalk.bold('User:')} ${user.email}`);
|
||||
if (user.firstName || user.lastName) {
|
||||
console.log(
|
||||
` ${chalk.gray('Name:')} ${user.firstName || ''} ${user.lastName || ''}`,
|
||||
);
|
||||
}
|
||||
console.log(` ${chalk.gray('ID:')} ${user.id}`);
|
||||
console.log(
|
||||
` ${chalk.gray('Member of:')} ${user.membership.length} organizations`,
|
||||
);
|
||||
console.log(` ${chalk.gray('Auth accounts:')} ${user.accounts.length}`);
|
||||
|
||||
if (user.createdOrganizations.length > 0) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`\n ⚠️ This user CREATED ${user.createdOrganizations.length} organization(s):`,
|
||||
),
|
||||
);
|
||||
for (const org of user.createdOrganizations) {
|
||||
console.log(` - ${org.name} ${chalk.gray(`(${org.id})`)}`);
|
||||
}
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
' Note: These organizations will NOT be deleted, only the user reference.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (user.membership.length > 0) {
|
||||
console.log(
|
||||
chalk.red('\n Organizations where user will be removed from:'),
|
||||
);
|
||||
for (const member of user.membership) {
|
||||
console.log(
|
||||
` - ${member.organization.name} ${chalk.gray(`(${member.role})`)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red(
|
||||
'\n⚠️ This will delete the user account, all sessions, and remove them from all organizations!',
|
||||
),
|
||||
);
|
||||
|
||||
// First confirmation
|
||||
const { confirmFirst } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirmFirst',
|
||||
message: chalk.red(
|
||||
`Are you ABSOLUTELY SURE you want to delete user "${user.email}"?`,
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!confirmFirst) {
|
||||
console.log(chalk.yellow('\nDeletion cancelled.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Second confirmation - type email
|
||||
const { confirmEmail } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'confirmEmail',
|
||||
message: `Type the user email "${user.email}" to confirm deletion:`,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirmEmail !== user.email) {
|
||||
console.log(chalk.red('\n❌ Email does not match. Deletion cancelled.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Final confirmation
|
||||
const { confirmFinal } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirmFinal',
|
||||
message: chalk.red(
|
||||
'FINAL WARNING: This action CANNOT be undone. Delete now?',
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!confirmFinal) {
|
||||
console.log(chalk.yellow('\nDeletion cancelled.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.red('\n🗑️ Deleting user...\n'));
|
||||
|
||||
try {
|
||||
// Delete the user (cascade will handle related records like sessions, accounts, memberships)
|
||||
await db.user.delete({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(chalk.green('\n✅ User deleted successfully!'));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`Deleted: ${user.email} (removed from ${user.membership.length} organizations)`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red('\n❌ Error deleting user:'), error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
104
admin/src/commands/lookup-client.ts
Normal file
104
admin/src/commands/lookup-client.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { db } from '@openpanel/db';
|
||||
import chalk from 'chalk';
|
||||
import fuzzy from 'fuzzy';
|
||||
import inquirer from 'inquirer';
|
||||
import autocomplete from 'inquirer-autocomplete-prompt';
|
||||
import { displayOrganizationDetails } from '../utils/display';
|
||||
|
||||
// Register autocomplete prompt
|
||||
inquirer.registerPrompt('autocomplete', autocomplete);
|
||||
|
||||
interface ClientSearchItem {
|
||||
id: string;
|
||||
name: string;
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
projectId: string | null;
|
||||
projectName: string | null;
|
||||
displayText: string;
|
||||
}
|
||||
|
||||
export async function lookupByClient() {
|
||||
console.log(chalk.blue('\n🔑 Lookup by Client ID\n'));
|
||||
console.log('Loading clients...\n');
|
||||
|
||||
const clients = await db.client.findMany({
|
||||
include: {
|
||||
organization: true,
|
||||
project: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (clients.length === 0) {
|
||||
console.log(chalk.red('No clients found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const searchItems: ClientSearchItem[] = clients.map((client) => ({
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
organizationId: client.organizationId,
|
||||
organizationName: client.organization.name,
|
||||
projectId: client.projectId,
|
||||
projectName: client.project?.name || null,
|
||||
displayText: `${client.organization.name} → ${client.project?.name || '[No Project]'} → ${client.name} ${chalk.gray(`(${client.id})`)}`,
|
||||
}));
|
||||
|
||||
const searchFunction = async (_answers: unknown, input = '') => {
|
||||
const fuzzyResult = fuzzy.filter(input, searchItems, {
|
||||
extract: (item: ClientSearchItem) =>
|
||||
`${item.organizationName} ${item.projectName || ''} ${item.name} ${item.id}`,
|
||||
});
|
||||
|
||||
return fuzzyResult.map((result: fuzzy.FilterResult<ClientSearchItem>) => ({
|
||||
name: result.original.displayText,
|
||||
value: result.original,
|
||||
}));
|
||||
};
|
||||
|
||||
const { selectedClient } = (await inquirer.prompt([
|
||||
{
|
||||
type: 'autocomplete',
|
||||
name: 'selectedClient',
|
||||
message: 'Search for a client:',
|
||||
source: searchFunction,
|
||||
pageSize: 15,
|
||||
},
|
||||
])) as { selectedClient: ClientSearchItem };
|
||||
|
||||
// Fetch full organization details
|
||||
const organization = await db.organization.findUnique({
|
||||
where: {
|
||||
id: selectedClient.organizationId,
|
||||
},
|
||||
include: {
|
||||
projects: {
|
||||
include: {
|
||||
clients: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
},
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
console.log(chalk.red('Organization not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
displayOrganizationDetails(organization, {
|
||||
highlightProjectId: selectedClient.projectId || undefined,
|
||||
highlightClientId: selectedClient.id,
|
||||
});
|
||||
}
|
||||
|
||||
112
admin/src/commands/lookup-email.ts
Normal file
112
admin/src/commands/lookup-email.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { db } from '@openpanel/db';
|
||||
import chalk from 'chalk';
|
||||
import fuzzy from 'fuzzy';
|
||||
import inquirer from 'inquirer';
|
||||
import autocomplete from 'inquirer-autocomplete-prompt';
|
||||
import { displayOrganizationDetails } from '../utils/display';
|
||||
|
||||
// Register autocomplete prompt
|
||||
inquirer.registerPrompt('autocomplete', autocomplete);
|
||||
|
||||
interface EmailSearchItem {
|
||||
email: string;
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
role: string;
|
||||
userId: string | null;
|
||||
displayText: string;
|
||||
}
|
||||
|
||||
export async function lookupByEmail() {
|
||||
console.log(chalk.blue('\n📧 Lookup by Email\n'));
|
||||
console.log('Loading members...\n');
|
||||
|
||||
const members = await db.member.findMany({
|
||||
include: {
|
||||
organization: true,
|
||||
user: true,
|
||||
},
|
||||
orderBy: {
|
||||
email: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (members.length === 0) {
|
||||
console.log(chalk.red('No members found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by email (in case same email is in multiple orgs)
|
||||
const searchItems: EmailSearchItem[] = members.map((member) => {
|
||||
const email = member.user?.email || member.email || 'Unknown';
|
||||
const roleBadge =
|
||||
member.role === 'owner' ? '👑' : member.role === 'admin' ? '⭐' : '👤';
|
||||
|
||||
return {
|
||||
email,
|
||||
organizationId: member.organizationId,
|
||||
organizationName: member.organization.name,
|
||||
role: member.role,
|
||||
userId: member.userId,
|
||||
displayText: `${email} ${chalk.gray(`→ ${member.organization.name}`)} ${roleBadge}`,
|
||||
};
|
||||
});
|
||||
|
||||
const searchFunction = async (_answers: unknown, input = '') => {
|
||||
const fuzzyResult = fuzzy.filter(input, searchItems, {
|
||||
extract: (item: EmailSearchItem) =>
|
||||
`${item.email} ${item.organizationName}`,
|
||||
});
|
||||
|
||||
return fuzzyResult.map((result: fuzzy.FilterResult<EmailSearchItem>) => ({
|
||||
name: result.original.displayText,
|
||||
value: result.original,
|
||||
}));
|
||||
};
|
||||
|
||||
const { selectedMember } = (await inquirer.prompt([
|
||||
{
|
||||
type: 'autocomplete',
|
||||
name: 'selectedMember',
|
||||
message: 'Search for a member by email:',
|
||||
source: searchFunction,
|
||||
pageSize: 15,
|
||||
},
|
||||
])) as { selectedMember: EmailSearchItem };
|
||||
|
||||
// Fetch full organization details
|
||||
const organization = await db.organization.findUnique({
|
||||
where: {
|
||||
id: selectedMember.organizationId,
|
||||
},
|
||||
include: {
|
||||
projects: {
|
||||
include: {
|
||||
clients: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
},
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
console.log(chalk.red('Organization not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\nShowing organization for: ${selectedMember.email} (${selectedMember.role})\n`,
|
||||
),
|
||||
);
|
||||
|
||||
displayOrganizationDetails(organization);
|
||||
}
|
||||
|
||||
88
admin/src/commands/lookup-org.ts
Normal file
88
admin/src/commands/lookup-org.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { db } from '@openpanel/db';
|
||||
import chalk from 'chalk';
|
||||
import fuzzy from 'fuzzy';
|
||||
import inquirer from 'inquirer';
|
||||
import autocomplete from 'inquirer-autocomplete-prompt';
|
||||
import { displayOrganizationDetails } from '../utils/display';
|
||||
|
||||
// Register autocomplete prompt
|
||||
inquirer.registerPrompt('autocomplete', autocomplete);
|
||||
|
||||
interface OrgSearchItem {
|
||||
id: string;
|
||||
name: string;
|
||||
displayText: string;
|
||||
}
|
||||
|
||||
export async function lookupByOrg() {
|
||||
console.log(chalk.blue('\n🏢 Lookup by Organization\n'));
|
||||
console.log('Loading organizations...\n');
|
||||
|
||||
const organizations = await db.organization.findMany({
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (organizations.length === 0) {
|
||||
console.log(chalk.red('No organizations found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const searchItems: OrgSearchItem[] = organizations.map((org) => ({
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
displayText: `${org.name} ${chalk.gray(`(${org.id})`)}`,
|
||||
}));
|
||||
|
||||
const searchFunction = async (_answers: unknown, input = '') => {
|
||||
const fuzzyResult = fuzzy.filter(input, searchItems, {
|
||||
extract: (item: OrgSearchItem) => `${item.name} ${item.id}`,
|
||||
});
|
||||
|
||||
return fuzzyResult.map((result: fuzzy.FilterResult<OrgSearchItem>) => ({
|
||||
name: result.original.displayText,
|
||||
value: result.original,
|
||||
}));
|
||||
};
|
||||
|
||||
const { selectedOrg } = (await inquirer.prompt([
|
||||
{
|
||||
type: 'autocomplete',
|
||||
name: 'selectedOrg',
|
||||
message: 'Search for an organization:',
|
||||
source: searchFunction,
|
||||
pageSize: 15,
|
||||
},
|
||||
])) as { selectedOrg: OrgSearchItem };
|
||||
|
||||
// Fetch full organization details
|
||||
const organization = await db.organization.findUnique({
|
||||
where: {
|
||||
id: selectedOrg.id,
|
||||
},
|
||||
include: {
|
||||
projects: {
|
||||
include: {
|
||||
clients: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
},
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
console.log(chalk.red('Organization not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
displayOrganizationDetails(organization);
|
||||
}
|
||||
|
||||
98
admin/src/commands/lookup-project.ts
Normal file
98
admin/src/commands/lookup-project.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { db } from '@openpanel/db';
|
||||
import chalk from 'chalk';
|
||||
import fuzzy from 'fuzzy';
|
||||
import inquirer from 'inquirer';
|
||||
import autocomplete from 'inquirer-autocomplete-prompt';
|
||||
import { displayOrganizationDetails } from '../utils/display';
|
||||
|
||||
// Register autocomplete prompt
|
||||
inquirer.registerPrompt('autocomplete', autocomplete);
|
||||
|
||||
interface ProjectSearchItem {
|
||||
id: string;
|
||||
name: string;
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
displayText: string;
|
||||
}
|
||||
|
||||
export async function lookupByProject() {
|
||||
console.log(chalk.blue('\n📊 Lookup by Project\n'));
|
||||
console.log('Loading projects...\n');
|
||||
|
||||
const projects = await db.project.findMany({
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (projects.length === 0) {
|
||||
console.log(chalk.red('No projects found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const searchItems: ProjectSearchItem[] = projects.map((project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
organizationId: project.organizationId,
|
||||
organizationName: project.organization.name,
|
||||
displayText: `${project.organization.name} → ${project.name} ${chalk.gray(`(${project.id})`)}`,
|
||||
}));
|
||||
|
||||
const searchFunction = async (_answers: unknown, input = '') => {
|
||||
const fuzzyResult = fuzzy.filter(input, searchItems, {
|
||||
extract: (item: ProjectSearchItem) =>
|
||||
`${item.organizationName} ${item.name} ${item.id}`,
|
||||
});
|
||||
|
||||
return fuzzyResult.map((result: fuzzy.FilterResult<ProjectSearchItem>) => ({
|
||||
name: result.original.displayText,
|
||||
value: result.original,
|
||||
}));
|
||||
};
|
||||
|
||||
const { selectedProject } = (await inquirer.prompt([
|
||||
{
|
||||
type: 'autocomplete',
|
||||
name: 'selectedProject',
|
||||
message: 'Search for a project:',
|
||||
source: searchFunction,
|
||||
pageSize: 15,
|
||||
},
|
||||
])) as { selectedProject: ProjectSearchItem };
|
||||
|
||||
// Fetch full organization details
|
||||
const organization = await db.organization.findUnique({
|
||||
where: {
|
||||
id: selectedProject.organizationId,
|
||||
},
|
||||
include: {
|
||||
projects: {
|
||||
include: {
|
||||
clients: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
},
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
console.log(chalk.red('Organization not found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
displayOrganizationDetails(organization, {
|
||||
highlightProjectId: selectedProject.id,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user