Monorepo for Aesthetic.Computer
aesthetic.computer
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}