d312783f3a
Deploy to route.crispygoat.com / deploy (push) Successful in 3m59s
- getAdminUser now properly supports platform_admin with 0 brand links and loads full brand_ids - createAdminUser action now inserts into admin_user_brands join table - Admin layout surfaces the signed-in email on Access Denied - AdminAccessDenied links to /login instead of dead-end /admin - Main dashboard uses direct pool query instead of dead supabase shim - Improved provision-admin.ts script for prod bootstrap (loads .env.production too)
124 lines
4.0 KiB
TypeScript
124 lines
4.0 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 (or .env.production) manually so the script works in prod bootstrap
|
|
const envFiles = [".env.local", ".env.production"];
|
|
for (const f of envFiles) {
|
|
const envPath = path.join(process.cwd(), f);
|
|
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("=");
|
|
const k = key.trim();
|
|
if (!process.env[k]) {
|
|
process.env[k] = valueParts.join("=").trim();
|
|
}
|
|
}
|
|
}
|
|
console.log(`[provision] loaded ${f}`);
|
|
}
|
|
}
|
|
|
|
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 your production URL /admin (sign in first if needed).`);
|
|
|
|
} finally {
|
|
await pool.end();
|
|
}
|
|
}
|
|
|
|
main().catch(console.error);
|