Auto-create tenant for any hostname that reaches the app
DNS is the gatekeeper — if a request arrives, it was pointed here intentionally. Every new hostname gets its own tenant automatically, no manual setup or seeding required. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+10
-21
@@ -6,28 +6,19 @@ const prisma = new PrismaClient();
|
|||||||
async function main() {
|
async function main() {
|
||||||
console.log('Seeding database...');
|
console.log('Seeding database...');
|
||||||
|
|
||||||
// Create default tenant
|
// The app auto-creates a tenant on first request,
|
||||||
const tenant = await prisma.tenant.upsert({
|
// but the seed ensures an admin user exists.
|
||||||
where: { slug: 'default' },
|
let tenant = await prisma.tenant.findFirst();
|
||||||
update: {},
|
if (!tenant) {
|
||||||
create: {
|
tenant = await prisma.tenant.create({
|
||||||
name: 'Default Workspace',
|
data: {
|
||||||
|
name: 'Pounce',
|
||||||
slug: 'default'
|
slug: 'default'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
console.log(` Created tenant: ${tenant.name} (${tenant.id})`);
|
||||||
// Map hostnames to default tenant
|
} else {
|
||||||
const hostnames = ['localhost', '127.0.0.1', 'pounce.catrenelle.com'];
|
console.log(` Existing tenant: ${tenant.name} (${tenant.id})`);
|
||||||
for (const hostname of hostnames) {
|
|
||||||
await prisma.tenantHostname.upsert({
|
|
||||||
where: { hostname },
|
|
||||||
update: {},
|
|
||||||
create: {
|
|
||||||
hostname,
|
|
||||||
tenantId: tenant.id,
|
|
||||||
isPrimary: hostname === 'localhost'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create admin user
|
// Create admin user
|
||||||
@@ -46,8 +37,6 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('Seed complete.');
|
console.log('Seed complete.');
|
||||||
console.log(` Tenant: ${tenant.name} (${tenant.id})`);
|
|
||||||
console.log(` Hostnames: ${hostnames.join(', ')}`);
|
|
||||||
console.log(' Admin: admin@example.com / admin123');
|
console.log(' Admin: admin@example.com / admin123');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,23 +19,71 @@ export async function resolveTenant(hostname: string): Promise<TenantInfo | null
|
|||||||
return cached.tenant;
|
return cached.tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look up existing hostname mapping
|
||||||
const record = await prisma.tenantHostname.findUnique({
|
const record = await prisma.tenantHostname.findUnique({
|
||||||
where: { hostname: host },
|
where: { hostname: host },
|
||||||
include: { tenant: true }
|
include: { tenant: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!record) return null;
|
if (record) {
|
||||||
|
|
||||||
const tenant: TenantInfo = {
|
const tenant: TenantInfo = {
|
||||||
id: record.tenant.id,
|
id: record.tenant.id,
|
||||||
name: record.tenant.name,
|
name: record.tenant.name,
|
||||||
slug: record.tenant.slug
|
slug: record.tenant.slug
|
||||||
};
|
};
|
||||||
|
|
||||||
tenantCache.set(host, { tenant, expiresAt: Date.now() + CACHE_TTL });
|
tenantCache.set(host, { tenant, expiresAt: Date.now() + CACHE_TTL });
|
||||||
return tenant;
|
return tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No mapping found — auto-provision
|
||||||
|
// If a default tenant exists, map this hostname to it.
|
||||||
|
// If no tenants exist at all, create the first one.
|
||||||
|
const tenant = await autoProvisionTenant(host);
|
||||||
|
if (tenant) {
|
||||||
|
tenantCache.set(host, { tenant, expiresAt: Date.now() + CACHE_TTL });
|
||||||
|
}
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function autoProvisionTenant(host: string): Promise<TenantInfo> {
|
||||||
|
// Any hostname that reaches this app was pointed here via DNS,
|
||||||
|
// so it's intentional. Auto-create a tenant for it.
|
||||||
|
const slug = slugify(host);
|
||||||
|
const name = humanizeDomain(host);
|
||||||
|
|
||||||
|
const tenant = await prisma.tenant.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
hostnames: {
|
||||||
|
create: { hostname: host, isPrimary: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id: tenant.id, name: tenant.name, slug: tenant.slug };
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugify(host: string): string {
|
||||||
|
return host
|
||||||
|
.replace(/[^a-zA-Z0-9]/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.slice(0, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanizeDomain(host: string): string {
|
||||||
|
// "pounce.catrenelle.com" -> "Pounce"
|
||||||
|
// "192.168.3.65" -> "Pounce"
|
||||||
|
// "localhost" -> "Pounce"
|
||||||
|
const part = host.split('.')[0];
|
||||||
|
if (/^\d+$/.test(part) || part === 'localhost') {
|
||||||
|
return 'Pounce';
|
||||||
|
}
|
||||||
|
return part.charAt(0).toUpperCase() + part.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
export function clearTenantCache() {
|
export function clearTenantCache() {
|
||||||
tenantCache.clear();
|
tenantCache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user