229 lines
6.5 KiB
TypeScript
229 lines
6.5 KiB
TypeScript
import { ConnectionFactory } from "./database";
|
|
import { v4 as uuidV4 } from "uuid";
|
|
|
|
/**
|
|
* Migration script to add multi-provider and enhanced session support
|
|
* Run this script once to migrate existing database
|
|
*/
|
|
|
|
export async function migrateMultiAuth() {
|
|
const conn = ConnectionFactory();
|
|
|
|
|
|
try {
|
|
// Step 1: Check if UserProvider table exists
|
|
const tableCheck = await conn.execute({
|
|
sql: "SELECT name FROM sqlite_master WHERE type='table' AND name='UserProvider'"
|
|
});
|
|
|
|
if (tableCheck.rows.length > 0) {
|
|
|
|
} else {
|
|
|
|
await conn.execute(`
|
|
CREATE TABLE UserProvider (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
provider TEXT NOT NULL CHECK(provider IN ('email', 'google', 'github', 'apple')),
|
|
provider_user_id TEXT,
|
|
email TEXT,
|
|
display_name TEXT,
|
|
image TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
last_used_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
|
|
await conn.execute(
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_user_provider_provider_user ON UserProvider (provider, provider_user_id)"
|
|
);
|
|
await conn.execute(
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_user_provider_provider_email ON UserProvider (provider, email)"
|
|
);
|
|
await conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_user_provider_user_id ON UserProvider (user_id)"
|
|
);
|
|
await conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_user_provider_provider ON UserProvider (provider)"
|
|
);
|
|
await conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_user_provider_email ON UserProvider (email)"
|
|
);
|
|
}
|
|
|
|
// Step 2: Check if Session table has device columns
|
|
const sessionColumnsCheck = await conn.execute({
|
|
sql: "PRAGMA table_info(Session)"
|
|
});
|
|
const hasDeviceName = sessionColumnsCheck.rows.some(
|
|
(row: any) => row.name === "device_name"
|
|
);
|
|
|
|
if (hasDeviceName) {
|
|
|
|
} else {
|
|
|
|
await conn.execute("ALTER TABLE Session ADD COLUMN device_name TEXT");
|
|
await conn.execute("ALTER TABLE Session ADD COLUMN device_type TEXT");
|
|
await conn.execute("ALTER TABLE Session ADD COLUMN browser TEXT");
|
|
await conn.execute("ALTER TABLE Session ADD COLUMN os TEXT");
|
|
|
|
// SQLite doesn't support non-constant defaults in ALTER TABLE
|
|
// Add column with NULL default, then update existing rows
|
|
await conn.execute("ALTER TABLE Session ADD COLUMN last_active_at TEXT");
|
|
|
|
// Update existing rows to set last_active_at = last_used
|
|
|
|
await conn.execute(
|
|
"UPDATE Session SET last_active_at = COALESCE(last_used, created_at) WHERE last_active_at IS NULL"
|
|
);
|
|
|
|
|
|
await conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_session_last_active ON Session (last_active_at)"
|
|
);
|
|
await conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_session_user_active ON Session (user_id, revoked, last_active_at)"
|
|
);
|
|
}
|
|
|
|
// Step 3: Migrate existing users to UserProvider table
|
|
|
|
const usersResult = await conn.execute({
|
|
sql: "SELECT id, email, provider, display_name, image, apple_user_string FROM User WHERE provider IS NOT NULL"
|
|
});
|
|
|
|
|
|
|
|
let migratedCount = 0;
|
|
for (const row of usersResult.rows) {
|
|
const user = row as any;
|
|
|
|
// Skip apple provider users (they're for Life and Lineage mobile app, not website auth)
|
|
if (user.provider === "apple") {
|
|
|
|
continue;
|
|
}
|
|
|
|
// Check if already migrated
|
|
const existingProvider = await conn.execute({
|
|
sql: "SELECT id FROM UserProvider WHERE user_id = ? AND provider = ?",
|
|
args: [user.id, user.provider || "email"]
|
|
});
|
|
|
|
if (existingProvider.rows.length > 0) {
|
|
|
|
continue;
|
|
}
|
|
|
|
// Determine provider_user_id based on provider type
|
|
let providerUserId: string | null = null;
|
|
if (user.provider === "github") {
|
|
providerUserId = user.display_name;
|
|
} else if (user.provider === "google") {
|
|
providerUserId = user.email;
|
|
} else {
|
|
providerUserId = user.email;
|
|
}
|
|
|
|
try {
|
|
await conn.execute({
|
|
sql: `INSERT INTO UserProvider (id, user_id, provider, provider_user_id, email, display_name, image)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
args: [
|
|
uuidV4(),
|
|
user.id,
|
|
user.provider || "email",
|
|
providerUserId,
|
|
user.email,
|
|
user.display_name,
|
|
user.image
|
|
]
|
|
});
|
|
migratedCount++;
|
|
} catch (error: any) {
|
|
console.error(
|
|
`[Migration] Failed to migrate user ${user.id}:`,
|
|
error.message
|
|
);
|
|
}
|
|
}
|
|
|
|
// Determine provider_user_id based on provider type
|
|
let providerUserId: string | null = null;
|
|
if (user.provider === "github") {
|
|
providerUserId = user.display_name;
|
|
} else if (user.provider === "google") {
|
|
providerUserId = user.email;
|
|
} else if (user.provider === "apple") {
|
|
providerUserId = user.apple_user_string;
|
|
} else {
|
|
providerUserId = user.email;
|
|
}
|
|
|
|
try {
|
|
await conn.execute({
|
|
sql: `INSERT INTO UserProvider (id, user_id, provider, provider_user_id, email, display_name, image)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
args: [
|
|
uuidV4(),
|
|
user.id,
|
|
user.provider || "email",
|
|
providerUserId,
|
|
user.email,
|
|
user.display_name,
|
|
user.image
|
|
]
|
|
});
|
|
migratedCount++;
|
|
} catch (error: any) {
|
|
console.error(
|
|
`[Migration] Failed to migrate user ${user.id}:`,
|
|
error.message
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Step 4: Verification
|
|
|
|
const providerCount = await conn.execute({
|
|
sql: "SELECT COUNT(*) as count FROM UserProvider"
|
|
});
|
|
|
|
|
|
const multiProviderUsers = await conn.execute({
|
|
sql: `SELECT COUNT(*) as count FROM (
|
|
SELECT user_id FROM UserProvider GROUP BY user_id HAVING COUNT(*) > 1
|
|
)`
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
success: true,
|
|
migratedUsers: migratedCount,
|
|
totalProviders: (providerCount.rows[0] as any).count
|
|
};
|
|
} catch (error) {
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Run migration if called directly
|
|
if (require.main === module) {
|
|
migrateMultiAuth()
|
|
.then((result) => {
|
|
|
|
process.exit(0);
|
|
})
|
|
.catch((error) => {
|
|
|
|
process.exit(1);
|
|
});
|
|
}
|