Monorepo for Aesthetic.Computer aesthetic.computer
at main 119 lines 4.1 kB view raw
1// device-auth.mjs — Device code auth flow for AC Native 2// 3// Flow: 4// 1. Device GET /api/device-auth?action=request → { code, authUrl, pollUrl } 5// 2. User visits authUrl on phone → logs in via Claude OAuth 6// 3. Phone POST /api/device-auth { action: "approve", code, credentials } 7// 4. Device GET /api/device-auth?action=poll&code=WOLF-3847 → { status, credentials } 8 9import { MongoClient } from "mongodb"; 10 11let mongoClient; 12async function getDb() { 13 if (!mongoClient) { 14 const uri = process.env.MONGODB_CONNECTION_STRING; 15 if (!uri) throw new Error("No MONGODB_CONNECTION_STRING"); 16 mongoClient = new MongoClient(uri); 17 await mongoClient.connect(); 18 } 19 return mongoClient.db("aesthetic"); 20} 21 22const WORDS = [ 23 "wolf", "bear", "deer", "hawk", "lynx", "fox", "owl", "elk", 24 "crab", "moth", "frog", "crow", "wasp", "newt", "wren", "dove", 25 "hare", "mink", "swan", "toad", "lark", "colt", "lamb", "puma", 26]; 27 28function generateCode() { 29 const word = WORDS[Math.floor(Math.random() * WORDS.length)].toUpperCase(); 30 const num = String(Math.floor(1000 + Math.random() * 9000)); 31 return `${word}-${num}`; 32} 33 34function json(statusCode, data) { 35 return { 36 statusCode, 37 headers: { 38 "Content-Type": "application/json", 39 "Access-Control-Allow-Origin": "*", 40 "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 41 "Access-Control-Allow-Headers": "Content-Type", 42 }, 43 body: JSON.stringify(data), 44 }; 45} 46 47export async function handler(event) { 48 if (event.httpMethod === "OPTIONS") { 49 return json(200, { ok: true }); 50 } 51 52 try { 53 const params = new URLSearchParams(event.rawQuery || ""); 54 let action, code, credentials; 55 56 if (event.httpMethod === "GET") { 57 action = params.get("action") || "poll"; 58 code = params.get("code"); 59 } else { 60 const body = JSON.parse(event.body || "{}"); 61 action = body.action; 62 code = body.code; 63 credentials = body.credentials; 64 } 65 66 if (action === "request") { 67 const db = await getDb(); 68 const col = db.collection("device_auth"); 69 await col.deleteMany({ createdAt: { $lt: new Date(Date.now() - 10 * 60 * 1000) } }); 70 71 let newCode; 72 for (let i = 0; i < 10; i++) { 73 newCode = generateCode(); 74 const exists = await col.findOne({ _id: newCode }); 75 if (!exists) break; 76 } 77 78 await col.insertOne({ _id: newCode, status: "pending", createdAt: new Date() }); 79 const baseUrl = process.env.URL || "https://aesthetic.computer"; 80 81 return json(200, { 82 code: newCode, 83 authUrl: `${baseUrl}/api/device-login?code=${newCode}`, 84 pollUrl: `${baseUrl}/api/device-auth?action=poll&code=${newCode}`, 85 expiresIn: 600, 86 }); 87 } 88 89 if (action === "approve") { 90 if (!code || !credentials) return json(400, { error: "missing code or credentials" }); 91 const db = await getDb(); 92 const col = db.collection("device_auth"); 93 const doc = await col.findOne({ _id: code }); 94 if (!doc) return json(404, { error: "code not found or expired" }); 95 if (doc.status !== "pending") return json(409, { error: "code already used" }); 96 await col.updateOne({ _id: code }, { $set: { status: "approved", credentials, approvedAt: new Date() } }); 97 return json(200, { ok: true }); 98 } 99 100 if (action === "poll") { 101 if (!code) return json(400, { error: "missing code" }); 102 const db = await getDb(); 103 const col = db.collection("device_auth"); 104 const doc = await col.findOne({ _id: code }); 105 if (!doc) return json(404, { error: "code not found or expired" }); 106 if (doc.status === "pending") return json(200, { status: "pending" }); 107 if (doc.status === "approved") { 108 await col.deleteOne({ _id: code }); 109 return json(200, { status: "approved", credentials: doc.credentials }); 110 } 111 return json(200, { status: doc.status }); 112 } 113 114 return json(400, { error: "unknown action" }); 115 } catch (err) { 116 console.error("device-auth error:", err); 117 return json(500, { error: "server error", message: err.message }); 118 } 119}