c86e97e816
Deploy to route.crispygoat.com / deploy (push) Failing after 7s
- 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
117 lines
3.7 KiB
TypeScript
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);
|