Fix SMTP delivery, add password reset and user deletion

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>
This commit is contained in:
Krikorios
2026-08-23 22:41:41 +03:00
co-authored by Claude Fable 5
parent a0b297285a
commit eceb6b45d5
20 changed files with 899 additions and 206 deletions
+68 -48
View File
@@ -1,5 +1,28 @@
import { requireOwner, withCors, preflight } from "@/lib/admin";
import { sql } from "@/lib/db";
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();
@@ -12,55 +35,52 @@ export async function GET(request: Request) {
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 rows = status
? await sql`
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
WHERE LOWER(r.payment_status) = ${status}
ORDER BY r.created_at DESC
LIMIT 500
`
: await sql`
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
ORDER BY r.created_at DESC
LIMIT 500
`;
const conds: string[] = [];
const params: SqlValue[] = [];
return withCors(Response.json({ data: rows }));
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(