feat(security): remove hardcoded admin/admin123 seed; first registered user is admin

This commit is contained in:
Jannis Braun
2026-06-20 02:07:47 +02:00
parent 3c134fe0e8
commit ae61786335
4 changed files with 107 additions and 83 deletions
@@ -0,0 +1,50 @@
import { describe, it, expect, beforeEach } from 'vitest';
import Database from 'better-sqlite3';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ensureDefaults } from './migrate.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function applyMigrations(db: Database.Database): void {
const migrationsDir = path.resolve(__dirname, '../../drizzle');
const files = fs.readdirSync(migrationsDir).filter(f => f.endsWith('.sql')).sort();
for (const f of files) {
const sqlText = fs.readFileSync(path.join(migrationsDir, f), 'utf8');
for (const stmt of sqlText.split(/-->\s*statement-breakpoint/)) {
const clean = stmt.trim();
if (clean) db.exec(clean);
}
}
}
describe('fresh instance has no seeded credentials', () => {
let sqlite: Database.Database;
beforeEach(() => {
sqlite = new Database(':memory:');
sqlite.pragma('foreign_keys = ON');
applyMigrations(sqlite);
ensureDefaults(sqlite);
});
it('creates no admin user and no default space on fresh boot', () => {
const users = sqlite.prepare('SELECT COUNT(*) AS n FROM users').get() as { n: number };
const spaces = sqlite.prepare('SELECT COUNT(*) AS n FROM spaces').get() as { n: number };
expect(users.n).toBe(0);
expect(spaces.n).toBe(0);
});
it('has no user named "admin"', () => {
const admin = sqlite.prepare("SELECT id FROM users WHERE username = 'admin'").get();
expect(admin).toBeUndefined();
});
it('still creates the singleton instance_settings row', () => {
const row = sqlite.prepare('SELECT id, worker_id FROM instance_settings WHERE id = 1').get() as
{ id: number; worker_id: number | null } | undefined;
expect(row?.id).toBe(1);
expect(row?.worker_id).not.toBeNull();
});
});
-81
View File
@@ -1,81 +0,0 @@
import { getDb, schema } from './index.js';
import { generateSnowflake } from '../utils/snowflake.js';
import { hashPassword } from '../utils/auth.js';
import { eq } from 'drizzle-orm';
import { DEFAULT_EVERYONE_PERMISSIONS, permissionsToString } from '@backspace/shared/src/permissions.js';
export async function seedDatabase(): Promise<void> {
const db = getDb();
const existingSpaces = db.select().from(schema.spaces).all();
if (existingSpaces.length > 0) {
console.log('Database already has data, skipping seed');
return;
}
console.log('Seeding database with default data...');
const adminId = generateSnowflake();
const adminPasswordHash = await hashPassword('admin123');
db.insert(schema.users).values({
id: adminId,
username: 'admin',
displayName: 'Admin',
passwordHash: adminPasswordHash,
status: 'offline',
isAdmin: 1,
createdAt: Date.now(),
}).run();
const spaceId = generateSnowflake();
db.insert(schema.spaces).values({
id: spaceId,
name: 'Backspace',
ownerId: adminId,
inviteCode: 'backspace',
createdAt: Date.now(),
}).run();
db.insert(schema.spaceMembers).values({
spaceId: spaceId,
userId: adminId,
joinedAt: Date.now(),
}).run();
const generalChannelId = generateSnowflake();
db.insert(schema.channels).values({
id: generalChannelId,
spaceId: spaceId,
name: 'general',
type: 'text',
topic: 'General discussion',
position: 0,
createdAt: Date.now(),
}).run();
const voiceChannelId = generateSnowflake();
db.insert(schema.channels).values({
id: voiceChannelId,
spaceId: spaceId,
name: 'General Voice',
type: 'voice',
position: 1,
createdAt: Date.now(),
}).run();
// Create @everyone role (id === spaceId convention)
db.insert(schema.roles).values({
id: spaceId,
spaceId: spaceId,
name: '@everyone',
color: '#b9bbbe',
position: 0,
permissions: permissionsToString(DEFAULT_EVERYONE_PERMISSIONS),
createdAt: Date.now(),
}).run();
console.log('Database seeded successfully');
console.log(` Default space: Backspace (invite code: backspace)`);
console.log(` Admin user: admin / admin123`);
}