SMTP: - Add connection/greeting/socket timeouts so a stalled Gmail connection no longer hangs sign-up - Wrap sendMail in try/catch and fall back to logging the code - Derive secure from port (465 implicit TLS vs 587 STARTTLS) - Strip whitespace from the Gmail app password - Document SMTP_HOST/SMTP_PORT in .env.example and environment.d.ts Password reset (new): - POST /(api)/auth/forgot-password emails a 6-digit code and does not reveal whether the address is registered - POST /(api)/auth/reset-password validates the code, sets the new password, verifies the email, and signs the user in - password_reset_codes table added to seed-db.mjs - "Forgot password?" flow on the mobile sign-in screen User deletion (new): - DELETE /(api)/admin/users/[id], owner-only, blocks self-deletion - Delete button with confirmation on the dashboard Users page Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
91 lines
2.4 KiB
TypeScript
91 lines
2.4 KiB
TypeScript
import { requireOwner, withCors, preflight } from "@/lib/admin";
|
|
import { query, type SqlValue } from "@/lib/db";
|
|
|
|
const PAGE_SIZE = 25;
|
|
|
|
const SELECT_RIDES = `
|
|
SELECT
|
|
r.ride_id,
|
|
r.origin_address,
|
|
r.destination_address,
|
|
r.ride_time,
|
|
r.fare_price,
|
|
r.payment_status,
|
|
r.created_at,
|
|
u.id AS user_id,
|
|
u.email AS user_email,
|
|
json_build_object(
|
|
'driver_id', d.id,
|
|
'name', d.first_name || ' ' || d.last_name,
|
|
'rating', d.rating
|
|
) AS driver
|
|
FROM rides r
|
|
INNER JOIN drivers d ON d.id = r.driver_id
|
|
INNER JOIN users u ON u.id = r.user_id
|
|
`;
|
|
|
|
export async function OPTIONS() {
|
|
return preflight();
|
|
}
|
|
|
|
export async function GET(request: Request) {
|
|
const auth = await requireOwner(request);
|
|
if ("error" in auth) return withCors(auth.error);
|
|
|
|
try {
|
|
const url = new URL(request.url);
|
|
const status = url.searchParams.get("status")?.trim().toLowerCase() ?? "";
|
|
const q = url.searchParams.get("q")?.trim() ?? "";
|
|
const page = Math.max(1, Number(url.searchParams.get("page")) || 1);
|
|
|
|
const conds: string[] = [];
|
|
const params: SqlValue[] = [];
|
|
|
|
if (status) {
|
|
params.push(status);
|
|
conds.push(`LOWER(r.payment_status) = $${params.length}`);
|
|
}
|
|
|
|
if (q) {
|
|
params.push(`%${q}%`);
|
|
const n = params.length;
|
|
conds.push(
|
|
`(u.email ILIKE $${n} OR (d.first_name || ' ' || d.last_name) ILIKE $${n} OR ` +
|
|
`r.origin_address ILIKE $${n} OR r.destination_address ILIKE $${n})`,
|
|
);
|
|
}
|
|
|
|
const where = conds.length ? ` WHERE ${conds.join(" AND ")}` : "";
|
|
|
|
const [{ count }] = await query<{ count: number }>(
|
|
`SELECT COUNT(*)::int AS count
|
|
FROM rides r
|
|
INNER JOIN drivers d ON d.id = r.driver_id
|
|
INNER JOIN users u ON u.id = r.user_id${where}`,
|
|
params,
|
|
);
|
|
|
|
const rows = await query(
|
|
`${SELECT_RIDES}${where}
|
|
ORDER BY r.created_at DESC
|
|
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
|
|
[...params, PAGE_SIZE, (page - 1) * PAGE_SIZE],
|
|
);
|
|
|
|
return withCors(
|
|
Response.json({
|
|
data: rows,
|
|
total: count,
|
|
page,
|
|
pageSize: PAGE_SIZE,
|
|
pages: Math.max(1, Math.ceil(count / PAGE_SIZE)),
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
console.error("[ADMIN_RIDES]: ", error);
|
|
return withCors(
|
|
Response.json({ error: "Internal Server Error" }, { status: 500 }),
|
|
);
|
|
}
|
|
}
|