Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2// audit-user-creation-sync.mjs
3// Comprehensive audit of user creation flow from Auth0 -> MongoDB -> ATProto
4// Checks: Auth0 signups, verifications collection, users collection, @handles collection, and ATProto accounts
5
6import { shell } from '../../system/backend/shell.mjs';
7import { connect } from '../../system/backend/database.mjs';
8import { userEmailFromID } from '../../system/backend/authorization.mjs';
9import { config } from 'dotenv';
10import { AtpAgent } from '@atproto/api';
11
12config();
13
14const PDS_URL = process.env.PDS_URL || 'https://at.aesthetic.computer';
15
16// Get Auth0 access token for a tenant
17async function getAuth0Token(tenant = 'aesthetic') {
18 const { got } = await import('got');
19
20 const clientId = tenant === 'aesthetic'
21 ? process.env.AUTH0_M2M_CLIENT_ID
22 : process.env.SOTCE_AUTH0_M2M_CLIENT_ID;
23 const clientSecret = tenant === 'aesthetic'
24 ? process.env.AUTH0_M2M_SECRET
25 : process.env.SOTCE_AUTH0_M2M_SECRET;
26 const baseURI = tenant === 'aesthetic'
27 ? 'https://aesthetic.us.auth0.com'
28 : 'https://sotce.us.auth0.com';
29
30 const tokenResponse = await got.post(`${baseURI}/oauth/token`, {
31 json: {
32 client_id: clientId,
33 client_secret: clientSecret,
34 audience: `${baseURI}/api/v2/`,
35 grant_type: 'client_credentials',
36 },
37 responseType: 'json',
38 });
39
40 return { token: tokenResponse.body.access_token, baseURI };
41}
42
43// Get recent Auth0 users
44async function getAuth0RecentUsers(tenant = 'aesthetic', limit = 10) {
45 try {
46 const { got } = await import('got');
47 const { token, baseURI } = await getAuth0Token(tenant);
48
49 const response = await got(`${baseURI}/api/v2/users`, {
50 searchParams: {
51 sort: 'created_at:-1',
52 per_page: limit,
53 page: 0,
54 fields: 'user_id,email,email_verified,created_at',
55 include_fields: true,
56 },
57 headers: { Authorization: `Bearer ${token}` },
58 responseType: 'json',
59 });
60
61 return response.body;
62 } catch (error) {
63 shell.error(`Error querying Auth0 ${tenant}: ${error.message}`);
64 return null;
65 }
66}
67
68// Check if ATProto account exists and is accessible
69async function checkAtprotoAccount(did, password) {
70 const agent = new AtpAgent({ service: PDS_URL });
71 try {
72 await agent.login({ identifier: did, password });
73 return { exists: true, accessible: true };
74 } catch (error) {
75 if (error.message?.includes('not found') || error.message?.includes('Unknown')) {
76 return { exists: false, accessible: false, error: 'Account not found on PDS' };
77 }
78 return { exists: true, accessible: false, error: error.message };
79 }
80}
81
82// Main audit function
83async function auditUserCreationSync(tenant = 'aesthetic', limit = 10) {
84 console.log(`\n${'='.repeat(80)}`);
85 console.log(`🔍 AUDITING USER CREATION & ATPROTO SYNC - ${tenant.toUpperCase()} TENANT`);
86 console.log(`${'='.repeat(80)}\n`);
87
88 const database = await connect();
89 const users = database.db.collection('users');
90 const handles = database.db.collection('@handles');
91 const verifications = database.db.collection('verifications');
92
93 // 1. Get recent Auth0 signups
94 console.log('📋 STEP 1: Fetching recent Auth0 signups...\n');
95 const auth0Users = await getAuth0RecentUsers(tenant, limit);
96
97 if (!auth0Users || auth0Users.length === 0) {
98 console.log('❌ No Auth0 users found or error fetching users\n');
99 await database.disconnect();
100 return;
101 }
102
103 console.log(`✅ Found ${auth0Users.length} recent Auth0 users\n`);
104
105 // 2. Check each user's sync status
106 const results = [];
107
108 for (const auth0User of auth0Users) {
109 const sub = auth0User.user_id;
110 const email = auth0User.email;
111 const emailVerified = auth0User.email_verified;
112 const createdAt = new Date(auth0User.created_at);
113 const hoursAgo = (Date.now() - createdAt.getTime()) / (1000 * 60 * 60);
114
115 console.log(`\n${'─'.repeat(80)}`);
116 console.log(`👤 User: ${sub}`);
117 console.log(` Email: ${email || 'N/A'}`);
118 console.log(` Email Verified: ${emailVerified ? '✅' : '❌'}`);
119 console.log(` Created: ${createdAt.toISOString()} (${hoursAgo.toFixed(1)}h ago)`);
120
121 const userStatus = {
122 sub,
123 email,
124 emailVerified,
125 createdAt,
126 hoursAgo: hoursAgo.toFixed(1),
127 verification: null,
128 userRecord: null,
129 handleRecord: null,
130 atproto: null,
131 issues: [],
132 };
133
134 // Check verifications collection
135 const verification = await verifications.findOne({ _id: sub });
136 if (verification) {
137 console.log(` ✅ Verifications record: count=${verification.count}`);
138 userStatus.verification = verification;
139 } else {
140 console.log(` ❌ No verifications record found`);
141 userStatus.issues.push('Missing verifications record');
142 }
143
144 // Check users collection
145 const userRecord = await users.findOne({ _id: sub });
146 if (userRecord) {
147 console.log(` ✅ Users record found:`);
148 console.log(` Code: ${userRecord.code || 'N/A'}`);
149 console.log(` Created: ${userRecord.when ? new Date(userRecord.when).toISOString() : 'N/A'}`);
150
151 if (userRecord.atproto) {
152 console.log(` ATProto:`);
153 console.log(` DID: ${userRecord.atproto.did || 'N/A'}`);
154 console.log(` Handle: ${userRecord.atproto.handle || 'N/A'}`);
155 console.log(` Created: ${userRecord.atproto.created || 'N/A'}`);
156
157 // Check if ATProto account actually exists on PDS
158 if (userRecord.atproto.did && userRecord.atproto.password) {
159 console.log(` 🔍 Checking PDS account...`);
160 const pdsCheck = await checkAtprotoAccount(
161 userRecord.atproto.did,
162 userRecord.atproto.password
163 );
164
165 if (pdsCheck.accessible) {
166 console.log(` ✅ PDS account exists and is accessible`);
167 } else if (pdsCheck.exists) {
168 console.log(` ⚠️ PDS account exists but login failed: ${pdsCheck.error}`);
169 userStatus.issues.push(`ATProto login failed: ${pdsCheck.error}`);
170 } else {
171 console.log(` ❌ PDS account not found: ${pdsCheck.error}`);
172 userStatus.issues.push(`ATProto account missing on PDS`);
173 }
174
175 userStatus.atproto = {
176 ...userRecord.atproto,
177 pdsCheck,
178 };
179 } else {
180 console.log(` ⚠️ Missing DID or password`);
181 userStatus.issues.push('ATProto credentials incomplete');
182 }
183 } else if (emailVerified) {
184 console.log(` ⚠️ No ATProto data (but email is verified!)`);
185 userStatus.issues.push('ATProto account not created despite email verification');
186 } else {
187 console.log(` ℹ️ No ATProto data (email not verified yet)`);
188 }
189
190 userStatus.userRecord = userRecord;
191 } else {
192 console.log(` ❌ No users record found`);
193 userStatus.issues.push('Missing users record');
194 }
195
196 // Check @handles collection
197 const handleRecord = await handles.findOne({ _id: sub });
198 if (handleRecord) {
199 console.log(` ✅ Handle record: @${handleRecord.handle}`);
200 userStatus.handleRecord = handleRecord;
201 } else {
202 console.log(` ℹ️ No handle set yet (optional)`);
203 }
204
205 // Summary for this user
206 if (userStatus.issues.length === 0) {
207 console.log(` ✅ ALL CHECKS PASSED`);
208 } else {
209 console.log(` ⚠️ ISSUES FOUND:`);
210 userStatus.issues.forEach(issue => console.log(` - ${issue}`));
211 }
212
213 results.push(userStatus);
214 }
215
216 // 3. Generate summary report
217 console.log(`\n\n${'='.repeat(80)}`);
218 console.log(`📊 SUMMARY REPORT - ${tenant.toUpperCase()} TENANT`);
219 console.log(`${'='.repeat(80)}\n`);
220
221 const totalUsers = results.length;
222 const usersWithVerifications = results.filter(r => r.verification).length;
223 const usersWithUserRecord = results.filter(r => r.userRecord).length;
224 const usersWithHandle = results.filter(r => r.handleRecord).length;
225 const usersWithAtproto = results.filter(r => r.atproto).length;
226 const usersWithAtprotoOnPds = results.filter(r => r.atproto?.pdsCheck?.accessible).length;
227 const emailVerified = results.filter(r => r.emailVerified).length;
228 const usersWithIssues = results.filter(r => r.issues.length > 0).length;
229
230 console.log(`Total Users Checked: ${totalUsers}`);
231 console.log(`Email Verified: ${emailVerified}/${totalUsers}`);
232 console.log(`\nMongoDB Collections:`);
233 console.log(` Verifications: ${usersWithVerifications}/${totalUsers}`);
234 console.log(` Users: ${usersWithUserRecord}/${totalUsers}`);
235 console.log(` Handles: ${usersWithHandle}/${totalUsers}`);
236 console.log(`\nATProto Status:`);
237 console.log(` ATProto Data in Mongo: ${usersWithAtproto}/${totalUsers}`);
238 console.log(` ATProto Accounts on PDS: ${usersWithAtprotoOnPds}/${totalUsers}`);
239 console.log(`\nIssues:`);
240 console.log(` Users with Issues: ${usersWithIssues}/${totalUsers}`);
241
242 if (usersWithIssues > 0) {
243 console.log(`\n⚠️ DETAILED ISSUES:\n`);
244 results.filter(r => r.issues.length > 0).forEach(user => {
245 console.log(` ${user.sub}:`);
246 user.issues.forEach(issue => console.log(` - ${issue}`));
247 });
248 }
249
250 // 4. Check for orphaned records (in Mongo but not in Auth0)
251 console.log(`\n\n${'='.repeat(80)}`);
252 console.log(`🔍 CHECKING FOR ORPHANED RECORDS`);
253 console.log(`${'='.repeat(80)}\n`);
254
255 const allAuth0Subs = new Set(auth0Users.map(u => u.user_id));
256
257 // Check recent user records
258 const recentUserRecords = await users.find({})
259 .sort({ when: -1 })
260 .limit(limit)
261 .toArray();
262
263 const orphanedUsers = recentUserRecords.filter(u => !allAuth0Subs.has(u._id));
264 if (orphanedUsers.length > 0) {
265 console.log(`⚠️ Found ${orphanedUsers.length} user records not in recent Auth0 users:`);
266 orphanedUsers.forEach(u => {
267 console.log(` - ${u._id} (created: ${u.when})`);
268 });
269 } else {
270 console.log(`✅ No orphaned user records found in recent ${limit} records`);
271 }
272
273 await database.disconnect();
274
275 console.log(`\n${'='.repeat(80)}`);
276 console.log(`✅ AUDIT COMPLETE`);
277 console.log(`${'='.repeat(80)}\n`);
278
279 return results;
280}
281
282// Run audit for both tenants
283const tenantArg = process.argv[2] || 'both';
284const limitArg = parseInt(process.argv[3]) || 10;
285
286console.log(`\n⏰ Audit started at: ${new Date().toISOString()}\n`);
287
288try {
289 if (tenantArg === 'both' || tenantArg === 'aesthetic') {
290 await auditUserCreationSync('aesthetic', limitArg);
291 }
292
293 if (tenantArg === 'both' || tenantArg === 'sotce') {
294 console.log('\n\n');
295 await auditUserCreationSync('sotce', limitArg);
296 }
297} catch (error) {
298 console.error('❌ Audit failed:', error);
299 process.exit(1);
300}
301
302console.log(`\n⏰ Audit completed at: ${new Date().toISOString()}\n`);