Monorepo for Aesthetic.Computer
aesthetic.computer
1// auth-cli-callback.mjs, 25.01.11
2// OAuth callback endpoint for CLI authentication
3// Receives authorization code and returns tokens via simple HTML page
4
5import { respond } from "../../backend/http.mjs";
6
7export async function handler(event, context) {
8 const params = new URLSearchParams(event.rawQuery || "");
9 const code = params.get("code");
10 const state = params.get("state");
11 const error = params.get("error");
12 const errorDescription = params.get("error_description");
13
14 // Handle OAuth errors
15 if (error) {
16 return respond(400, null, `
17 <!DOCTYPE html>
18 <html>
19 <head>
20 <title>Auth Failed - Aesthetic Computer</title>
21 <style>
22 body { font-family: monospace; max-width: 600px; margin: 50px auto; padding: 20px; }
23 .error { color: red; }
24 code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
25 </style>
26 </head>
27 <body>
28 <h1>❌ Authentication Failed</h1>
29 <p class="error"><strong>Error:</strong> ${error}</p>
30 <p><strong>Description:</strong> ${errorDescription || "Unknown error"}</p>
31 <hr>
32 <p>You can close this window and try again in your terminal.</p>
33 </body>
34 </html>
35 `, { "content-type": "text/html" });
36 }
37
38 // Validate required parameters
39 if (!code || !state) {
40 return respond(400, null, `
41 <!DOCTYPE html>
42 <html>
43 <head>
44 <title>Invalid Request - Aesthetic Computer</title>
45 <style>
46 body { font-family: monospace; max-width: 600px; margin: 50px auto; padding: 20px; }
47 </style>
48 </head>
49 <body>
50 <h1>⚠️ Invalid Request</h1>
51 <p>Missing required parameters (code or state).</p>
52 <p>This endpoint should only be accessed via OAuth redirect.</p>
53 </body>
54 </html>
55 `, { "content-type": "text/html" });
56 }
57
58 // Exchange code for tokens
59 const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
60 const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
61 const AUTH0_CLIENT_SECRET = process.env.AUTH0_CLIENT_SECRET;
62 const REDIRECT_URI = `https://aesthetic.computer/api/auth/cli-callback`;
63
64 try {
65 const tokenResponse = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
66 method: "POST",
67 headers: { "Content-Type": "application/json" },
68 body: JSON.stringify({
69 grant_type: "authorization_code",
70 client_id: AUTH0_CLIENT_ID,
71 client_secret: AUTH0_CLIENT_SECRET,
72 code: code,
73 redirect_uri: REDIRECT_URI,
74 }),
75 });
76
77 if (!tokenResponse.ok) {
78 const errorData = await tokenResponse.text();
79 console.error("Token exchange failed:", errorData);
80
81 return respond(500, null, `
82 <!DOCTYPE html>
83 <html>
84 <head>
85 <title>Token Exchange Failed - Aesthetic Computer</title>
86 <style>
87 body { font-family: monospace; max-width: 600px; margin: 50px auto; padding: 20px; }
88 .error { color: red; }
89 </style>
90 </head>
91 <body>
92 <h1>❌ Token Exchange Failed</h1>
93 <p class="error">Failed to exchange authorization code for tokens.</p>
94 <p>Please try again or check the server logs.</p>
95 </body>
96 </html>
97 `, { "content-type": "text/html" });
98 }
99
100 const tokens = await tokenResponse.json();
101
102 // Get user info
103 const userResponse = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, {
104 headers: { Authorization: `Bearer ${tokens.access_token}` },
105 });
106
107 const user = userResponse.ok ? await userResponse.json() : {};
108
109 // Return HTML page with tokens embedded (CLI will parse this)
110 return respond(200, null, `
111 <!DOCTYPE html>
112 <html>
113 <head>
114 <title>Authentication Success - Aesthetic Computer</title>
115 <style>
116 body {
117 font-family: monospace;
118 max-width: 600px;
119 margin: 50px auto;
120 padding: 20px;
121 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
122 color: white;
123 }
124 .success {
125 background: rgba(255,255,255,0.1);
126 padding: 20px;
127 border-radius: 8px;
128 backdrop-filter: blur(10px);
129 }
130 h1 { margin-top: 0; }
131 code {
132 background: rgba(0,0,0,0.3);
133 padding: 2px 6px;
134 border-radius: 3px;
135 display: inline-block;
136 }
137 .token-data {
138 display: none;
139 }
140 </style>
141 </head>
142 <body>
143 <div class="success">
144 <h1>✅ Authentication Successful</h1>
145 <p><strong>Welcome!</strong> ${user.email || user.name || "User"}</p>
146 <p>You are now logged in to <strong>Aesthetic Computer</strong>.</p>
147 <hr style="border-color: rgba(255,255,255,0.3);">
148 <p>You can now <strong>close this window</strong> and return to your terminal.</p>
149 <p style="font-size: 0.9em; opacity: 0.8;">Your CLI tool has received the authentication tokens.</p>
150 </div>
151
152 <!-- Token data for CLI parsing (hidden from user) -->
153 <div class="token-data">
154 <pre id="token-payload">${JSON.stringify({ tokens, user, state }, null, 2)}</pre>
155 </div>
156
157 <script>
158 // Optionally send token data to parent window if CLI opens in iframe
159 if (window.opener) {
160 try {
161 window.opener.postMessage({
162 type: 'ac-auth-success',
163 data: ${JSON.stringify({ tokens, user, state })}
164 }, '*');
165 } catch (e) {
166 console.error('Failed to post message to opener:', e);
167 }
168 }
169 </script>
170 </body>
171 </html>
172 `, { "content-type": "text/html" });
173
174 } catch (err) {
175 console.error("Callback error:", err);
176 return respond(500, null, `
177 <!DOCTYPE html>
178 <html>
179 <head>
180 <title>Server Error - Aesthetic Computer</title>
181 <style>
182 body { font-family: monospace; max-width: 600px; margin: 50px auto; padding: 20px; }
183 .error { color: red; }
184 </style>
185 </head>
186 <body>
187 <h1>❌ Server Error</h1>
188 <p class="error">An unexpected error occurred during authentication.</p>
189 <p>Please try again or contact support.</p>
190 </body>
191 </html>
192 `, { "content-type": "text/html" });
193 }
194}