Files
route-commerce/scripts/provision-admin.ts
T
openclaw c434015829
Deploy to route.crispygoat.com / deploy (push) Failing after 17s
feat(storage): MinIO object storage, Neon Auth, Supabase removal
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
  deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
  new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
  Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
  brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
  bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
  TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
  public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
  wholesale_settings lookup
2026-06-09 11:51:32 -06:00

117 lines
3.7 KiB
TypeScript

/**
* Provision an admin user by email.
* Run: npx tsx scripts/provision-admin.ts [email] [role]
*
* Role options: platform_admin, brand_admin, store_employee
*/
import pg from "pg";
import * as fs from "fs";
import * as path from "path";
// Load .env.local manually
const envPath = path.join(process.cwd(), ".env.local");
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, "utf-8");
for (const line of envContent.split("\n")) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#")) {
const [key, ...valueParts] = trimmed.split("=");
process.env[key.trim()] = valueParts.join("=").trim();
}
}
}
const { Pool } = pg;
async function main() {
const email = process.argv[2] ?? "admin@example.com";
const role = process.argv[3] ?? "platform_admin";
console.log("DATABASE_URL:", process.env.DATABASE_URL ? "set" : "NOT SET");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
try {
// Find the Neon Auth user by email
const userResult = await pool.query(
"SELECT id FROM neon_auth.user WHERE email = $1",
[email]
);
if (userResult.rows.length === 0) {
console.error(`User ${email} not found in neon_auth.user`);
console.error("Please sign up first at /login");
process.exit(1);
}
const userId = userResult.rows[0].id;
console.log(`Found Neon Auth user: ${userId} (${email})`);
// Check if already in admin_users
const existingAdmin = await pool.query(
"SELECT id FROM admin_users WHERE user_id = $1",
[userId]
);
let adminUserId: string;
if (existingAdmin.rows.length > 0) {
adminUserId = existingAdmin.rows[0].id;
console.log(`User already in admin_users: ${adminUserId}`);
} else {
// Insert into admin_users
const insertResult = await pool.query(
`INSERT INTO admin_users (user_id, email, name, role,
can_manage_orders, can_manage_products, can_manage_stops,
can_manage_customers, can_manage_wholesale, can_manage_billing,
can_manage_settings, can_manage_water_log, can_manage_time_tracking,
can_manage_route_trace, can_manage_reports, can_manage_communications)
VALUES ($1, $2, $3, $4, true, true, true, true, true, true, true, true, true, true, true, true)
RETURNING id`,
[userId, email, email.split("@")[0], role]
);
adminUserId = insertResult.rows[0].id;
console.log(`Created admin_users record: ${adminUserId}`);
}
// Get brands
const brandsResult = await pool.query("SELECT id, name, slug FROM brands LIMIT 10");
if (brandsResult.rows.length === 0) {
console.error("No brands found. Please create a brand first.");
process.exit(1);
}
console.log("\nAvailable brands:");
brandsResult.rows.forEach((b, i) => {
console.log(` ${i + 1}. ${b.name} (${b.slug}) - ${b.id}`);
});
// Link to first brand (or create a default one)
const brand = brandsResult.rows[0];
// Check if already linked
const existingLink = await pool.query(
"SELECT 1 FROM admin_user_brands WHERE admin_user_id = $1 AND brand_id = $2",
[adminUserId, brand.id]
);
if (existingLink.rows.length > 0) {
console.log(`Already linked to brand: ${brand.name}`);
} else {
await pool.query(
"INSERT INTO admin_user_brands (admin_user_id, brand_id) VALUES ($1, $2)",
[adminUserId, brand.id]
);
console.log(`Linked to brand: ${brand.name} (${brand.slug})`);
}
console.log(`\n✅ ${email} is now provisioned as ${role}!`);
console.log(` They can access the admin at http://localhost:4000/admin`);
} finally {
await pool.end();
}
}
main().catch(console.error);