my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

at go-port 240 lines 7.2 kB view raw
1#!/usr/bin/env bun 2/** 3 * Passkey Reset Script 4 * 5 * Resets a user's passkey credentials and generates a one-time reset link. 6 * The user can use this link to register a new passkey while preserving 7 * their existing account, permissions, and app authorizations. 8 * 9 * Usage: bun scripts/reset-passkey.ts <username> 10 * 11 * Example: 12 * bun scripts/reset-passkey.ts kieran 13 * 14 * The script will: 15 * 1. Verify the user exists 16 * 2. Delete all their existing passkey credentials 17 * 3. Invalidate all active sessions (logs them out) 18 * 4. Create a single-use reset invite locked to their username 19 * 5. Output a reset link 20 * 21 * IMPORTANT: This preserves: 22 * - User account and profile data 23 * - All app permissions and authorizations 24 * - Role assignments 25 * - Admin status 26 */ 27 28import { Database } from "bun:sqlite"; 29import crypto from "node:crypto"; 30import * as path from "node:path"; 31 32// Load database 33const dbPath = path.join(import.meta.dir, "..", "data", "indiko.db"); 34const db = new Database(dbPath); 35 36const ORIGIN = process.env.ORIGIN || "http://localhost:3000"; 37 38interface User { 39 id: number; 40 username: string; 41 name: string; 42 email: string | null; 43 status: string; 44 is_admin: number; 45} 46 47interface Credential { 48 id: number; 49 name: string | null; 50 created_at: number; 51} 52 53function getUser(username: string): User | null { 54 return db 55 .query("SELECT id, username, name, email, status, is_admin FROM users WHERE username = ?") 56 .get(username) as User | null; 57} 58 59function getCredentials(userId: number): Credential[] { 60 return db 61 .query("SELECT id, name, created_at FROM credentials WHERE user_id = ?") 62 .all(userId) as Credential[]; 63} 64 65function deleteCredentials(userId: number): number { 66 const result = db 67 .query("DELETE FROM credentials WHERE user_id = ?") 68 .run(userId); 69 return result.changes; 70} 71 72function deleteSessions(userId: number): number { 73 const result = db 74 .query("DELETE FROM sessions WHERE user_id = ?") 75 .run(userId); 76 return result.changes; 77} 78 79function createResetInvite(adminUserId: number, targetUsername: string): string { 80 const code = crypto.randomBytes(16).toString("base64url"); 81 const now = Math.floor(Date.now() / 1000); 82 const expiresAt = now + 86400; // 24 hours 83 84 // Check if there's a reset_username column, if not we'll use the note field 85 const hasResetColumn = db 86 .query("SELECT name FROM pragma_table_info('invites') WHERE name = 'reset_username'") 87 .get(); 88 89 if (hasResetColumn) { 90 db.query( 91 "INSERT INTO invites (code, created_by, max_uses, current_uses, expires_at, note, reset_username) VALUES (?, ?, 1, 0, ?, ?, ?)", 92 ).run(code, adminUserId, expiresAt, `Passkey reset for ${targetUsername}`, targetUsername); 93 } else { 94 // Use a special note format to indicate this is a reset invite 95 // Format: PASSKEY_RESET:username 96 db.query( 97 "INSERT INTO invites (code, created_by, max_uses, current_uses, expires_at, note, message) VALUES (?, ?, 1, 0, ?, ?, ?)", 98 ).run( 99 code, 100 adminUserId, 101 expiresAt, 102 `PASSKEY_RESET:${targetUsername}`, 103 `Your passkey has been reset. Please register a new passkey to regain access to your account.`, 104 ); 105 } 106 107 return code; 108} 109 110function getAdminUser(): User | null { 111 return db 112 .query("SELECT id, username, name, email, status, is_admin FROM users WHERE is_admin = 1 LIMIT 1") 113 .get() as User | null; 114} 115 116async function main() { 117 const args = process.argv.slice(2); 118 119 if (args.length === 0 || args.includes("--help") || args.includes("-h")) { 120 console.log(` 121Passkey Reset Script 122 123Usage: bun scripts/reset-passkey.ts <username> 124 125Options: 126 --help, -h Show this help message 127 --dry-run Show what would happen without making changes 128 --force Skip confirmation prompt 129 130Example: 131 bun scripts/reset-passkey.ts kieran 132 bun scripts/reset-passkey.ts kieran --dry-run 133`); 134 process.exit(0); 135 } 136 137 const username = args.find((arg) => !arg.startsWith("--")); 138 const dryRun = args.includes("--dry-run"); 139 const force = args.includes("--force"); 140 141 if (!username) { 142 console.error("❌ Error: Username is required"); 143 process.exit(1); 144 } 145 146 console.log(`\n🔐 Passkey Reset for: ${username}`); 147 console.log("─".repeat(50)); 148 149 // Look up user 150 const user = getUser(username); 151 if (!user) { 152 console.error(`\n❌ Error: User '${username}' not found`); 153 process.exit(1); 154 } 155 156 console.log(`\n📋 User Details:`); 157 console.log(` • ID: ${user.id}`); 158 console.log(` • Name: ${user.name}`); 159 console.log(` • Email: ${user.email || "(not set)"}`); 160 console.log(` • Status: ${user.status}`); 161 console.log(` • Admin: ${user.is_admin ? "Yes" : "No"}`); 162 163 // Get existing credentials 164 const credentials = getCredentials(user.id); 165 console.log(`\n🔑 Existing Passkeys: ${credentials.length}`); 166 credentials.forEach((cred, idx) => { 167 const date = new Date(cred.created_at * 1000).toISOString().split("T")[0]; 168 console.log(` ${idx + 1}. ${cred.name || "(unnamed)"} - created ${date}`); 169 }); 170 171 if (credentials.length === 0) { 172 console.log("\n⚠️ User has no passkeys registered. Creating reset link anyway..."); 173 } 174 175 if (dryRun) { 176 console.log("\n🔄 DRY RUN - No changes will be made"); 177 console.log("\nWould perform:"); 178 console.log(` • Delete ${credentials.length} passkey(s)`); 179 console.log(" • Invalidate all active sessions"); 180 console.log(" • Create single-use reset invite"); 181 process.exit(0); 182 } 183 184 // Confirmation prompt (unless --force) 185 if (!force) { 186 console.log("\n⚠️ This will:"); 187 console.log(` • Delete ALL ${credentials.length} passkey(s) for this user`); 188 console.log(" • Log them out of all sessions"); 189 console.log(" • Generate a 24-hour reset link\n"); 190 191 process.stdout.write("Continue? [y/N] "); 192 193 for await (const line of console) { 194 const answer = line.trim().toLowerCase(); 195 if (answer !== "y" && answer !== "yes") { 196 console.log("Cancelled."); 197 process.exit(0); 198 } 199 break; 200 } 201 } 202 203 // Get admin user for creating invite 204 const admin = getAdminUser(); 205 if (!admin) { 206 console.error("\n❌ Error: No admin user found to create invite"); 207 process.exit(1); 208 } 209 210 // Perform reset 211 console.log("\n🔄 Performing reset..."); 212 213 // Delete credentials 214 const deletedCreds = deleteCredentials(user.id); 215 console.log(` ✅ Deleted ${deletedCreds} passkey(s)`); 216 217 // Delete sessions 218 const deletedSessions = deleteSessions(user.id); 219 console.log(` ✅ Invalidated ${deletedSessions} session(s)`); 220 221 // Create reset invite 222 const inviteCode = createResetInvite(admin.id, username); 223 console.log(" ✅ Created reset invite"); 224 225 // Generate reset URL 226 const resetUrl = `${ORIGIN}/login?invite=${inviteCode}&username=${encodeURIComponent(username)}`; 227 228 console.log("\n" + "═".repeat(50)); 229 console.log("✨ PASSKEY RESET COMPLETE"); 230 console.log("═".repeat(50)); 231 console.log(`\n📧 Send this link to ${user.name || username}:\n`); 232 console.log(` ${resetUrl}`); 233 console.log(`\n⏰ This link expires in 24 hours and can only be used once.`); 234 console.log(`\n💡 The user must register with username: ${username}`); 235} 236 237main().catch((error) => { 238 console.error("\n❌ Error:", error instanceof Error ? error.message : error); 239 process.exit(1); 240});