/ opencodehub / src / pages / api / auth / reset-password.ts
swadhinbiswas feat: finalize platform polish - discussions, gists, webhook queue, realtime, push-mirror, auth/metrics hardening · 8/28/2026
f02976d
74 lines · 2.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { getDatabase, schema } from "@/db";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { passwordResetTokens, users } from "@/db/schema";
import { badRequest, parseBody, serverError, success } from "@/lib/api";
import { now } from "@/lib/utils";
import type { APIRoute } from "astro";
import bcrypt from "bcryptjs";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { logger } from "@/lib/logger";
import { withErrorHandler } from "@/lib/errors";

const resetPasswordSchema = z.object({
  token: z.string().min(1),
  password: z.string().min(8),
});

export const POST: APIRoute = withErrorHandler(async ({ request }) => {
  const parsed = await parseBody(request, resetPasswordSchema);
  if ("error" in parsed) return parsed.error;

  const { token, password } = parsed.data;
  const db = getDatabase() as NodePgDatabase<typeof schema>;

  // Find token
  const resetToken = await db.query.passwordResetTokens.findFirst({
    where: eq(passwordResetTokens.token, token),
  });

  if (!resetToken) {
    logger.warn({ token }, "Invalid password reset token used");
    return badRequest("Invalid or expired token");
  }

  // Check expiration
  if (new Date(resetToken.expiresAt) < new Date()) {
    // Delete expired token
    await db
      .delete(passwordResetTokens)
      .where(eq(passwordResetTokens.id, resetToken.id));
    logger.warn({ token }, "Expired password reset token used");
    return badRequest("Invalid or expired token");
  }

  // Hash new password
  const salt = await bcrypt.genSalt(10);
  const passwordHash = await bcrypt.hash(password, salt);

  // Update user password
  await db
    .update(users)
    .set({
      passwordHash,
      updatedAt: new Date(),
    })
    .where(eq(users.id, resetToken.userId));

  // Revoke all sessions — reset tokens prove account compromise recovery path
  await db
    .delete(schema.sessions)
    .where(eq(schema.sessions.userId, resetToken.userId));
  const { revokeUserSessionCache } = await import("@/lib/auth-cache");
  revokeUserSessionCache(resetToken.userId);

  // Delete used token
  await db
    .delete(passwordResetTokens)
    .where(eq(passwordResetTokens.id, resetToken.id));

  logger.info({ userId: resetToken.userId }, "Password reset completed via token");

  return success({ message: "Password reset successfully" });
});