tpmjs/packages/db/prisma/backfill-usernames.ts
Ajax Davis 77e697db56 fix: make username compulsory and improve MCP error messages
- Add profile settings page for username management
- Add username prompt in dashboard when not set
- Improve MCP endpoint to show specific error for missing user vs collection
- Make sign-up flow retry username PATCH and redirect to setup if fails
- Add backfill script for existing users without usernames
- Add Profile link to dashboard sidebar
2026-01-14 04:21:41 +10:00

155 lines
3.3 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Backfill usernames for all users who don't have one.
* Converts user's name to a URL-friendly slug.
*
* Run with: npx tsx packages/db/prisma/backfill-usernames.ts
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const RESERVED_USERNAMES = [
'admin',
'api',
'auth',
'dashboard',
'help',
'support',
'system',
'www',
'settings',
'login',
'logout',
'register',
'signup',
'signin',
'agents',
'collections',
'tools',
'tool',
'playground',
'explore',
'search',
'about',
'blog',
'docs',
'pricing',
'terms',
'privacy',
'contact',
'status',
'tpmjs',
'tpm',
'official',
];
/**
* Convert a display name to a URL-friendly username.
*/
function suggestUsername(name: string): string {
return name
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '') // Remove special chars
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Remove consecutive hyphens
.replace(/^-+|-+$/g, '') // Trim hyphens
.slice(0, 30);
}
/**
* Generate a unique username by appending numbers if needed.
*/
async function generateUniqueUsername(baseName: string): Promise<string> {
let username = suggestUsername(baseName);
// Ensure minimum length
if (username.length < 3) {
username = `user-${username || 'anon'}`;
}
// Check if reserved
if (RESERVED_USERNAMES.includes(username)) {
username = `${username}-user`;
}
// Check if already taken
const existing = await prisma.user.findUnique({
where: { username },
select: { id: true },
});
if (!existing) {
return username;
}
// Append numbers until unique
let counter = 1;
while (counter < 1000) {
const candidate = `${username.slice(0, 26)}-${counter}`;
const exists = await prisma.user.findUnique({
where: { username: candidate },
select: { id: true },
});
if (!exists) {
return candidate;
}
counter++;
}
// Fallback: use random suffix
return `${username.slice(0, 22)}-${Date.now().toString(36)}`;
}
async function main() {
console.log('🔧 Backfilling usernames for users without one...\n');
// Find all users without a username
const usersWithoutUsername = await prisma.user.findMany({
where: { username: null },
select: { id: true, name: true, email: true },
});
console.log(`Found ${usersWithoutUsername.length} users without a username.\n`);
if (usersWithoutUsername.length === 0) {
console.log('✅ All users already have usernames!');
return;
}
let updated = 0;
let failed = 0;
for (const user of usersWithoutUsername) {
try {
const username = await generateUniqueUsername(user.name);
await prisma.user.update({
where: { id: user.id },
data: { username },
});
console.log(`${user.email} → @${username}`);
updated++;
} catch (error) {
console.error(`❌ Failed to update ${user.email}:`, error);
failed++;
}
}
console.log(`\n${'='.repeat(50)}`);
console.log(`Updated: ${updated}`);
console.log(`Failed: ${failed}`);
console.log(`${'='.repeat(50)}`);
}
main()
.catch((e) => {
console.error('Backfill failed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});