File size: 3,139 Bytes
6a30288
 
 
 
 
 
 
 
 
 
 
 
f3845d0
 
 
 
 
 
 
 
 
 
 
 
 
 
6a30288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f3845d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a30288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f3845d0
 
 
 
 
6a30288
 
 
 
 
 
 
 
 
f3845d0
 
 
 
 
6a30288
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { db } from "@/db/db";
import { sessions, users } from "@/db/schema";
import { routes } from "@/lib/routes";
import { and, eq, gt } from "drizzle-orm";
import { randomBytes, scryptSync, timingSafeEqual } from "crypto";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { cache } from "react";

const SESSION_COOKIE_NAME = "shopsmart_session";
const SESSION_DURATION_SECONDS = 60 * 60 * 24 * 30;

function getSessionCookieOptions(expires: Date) {
  const isProduction = process.env.NODE_ENV === "production";
  const isHuggingFaceSpace = Boolean(process.env.SPACE_HOST);
  const useCrossSiteCookie = isProduction && isHuggingFaceSpace;

  return {
    httpOnly: true,
    sameSite: useCrossSiteCookie ? "none" : "lax",
    secure: isProduction,
    expires,
    path: "/",
  } as const;
}

export function hashPassword(password: string) {
  const salt = randomBytes(16).toString("hex");
  const hash = scryptSync(password, salt, 64).toString("hex");
  return `${salt}:${hash}`;
}

export function verifyPassword(password: string, storedHash: string | null) {
  if (!storedHash) return false;

  const [salt, storedKey] = storedHash.split(":");
  if (!salt || !storedKey) return false;

  const derivedKey = scryptSync(password, salt, 64);
  const storedKeyBuffer = Buffer.from(storedKey, "hex");

  if (derivedKey.length !== storedKeyBuffer.length) return false;

  return timingSafeEqual(derivedKey, storedKeyBuffer);
}

export const getCurrentUser = cache(async () => {
  const sessionToken = cookies().get(SESSION_COOKIE_NAME)?.value;

  if (!sessionToken) {
    return null;
  }

  try {
    const [record] = await db
      .select({
        id: users.id,
        name: users.name,
        email: users.email,
        passwordHash: users.passwordHash,
        storeId: users.storeId,
        createdAt: users.createdAt,
      })
      .from(sessions)
      .innerJoin(users, eq(sessions.userId, users.id))
      .where(
        and(
          eq(sessions.sessionToken, sessionToken),
          gt(sessions.expiresAt, Math.floor(Date.now() / 1000))
        )
      );

    return record ?? null;
  } catch (error) {
    console.error("Failed to load the current user from the session store.", error);
    return null;
  }
});

export async function requireUser() {
  const user = await getCurrentUser();

  if (!user) {
    redirect(routes.signIn);
  }

  return user;
}

export async function createSession(userId: number) {
  const sessionToken = randomBytes(32).toString("hex");
  const expiresAt =
    Math.floor(Date.now() / 1000) + SESSION_DURATION_SECONDS;

  await db.insert(sessions).values({
    sessionToken,
    userId,
    expiresAt,
  });

  cookies().set(
    SESSION_COOKIE_NAME,
    sessionToken,
    getSessionCookieOptions(new Date(expiresAt * 1000))
  );
}

export async function destroySession() {
  const sessionToken = cookies().get(SESSION_COOKIE_NAME)?.value;

  if (sessionToken) {
    await db.delete(sessions).where(eq(sessions.sessionToken, sessionToken));
  }

  cookies().set(
    SESSION_COOKIE_NAME,
    "",
    getSessionCookieOptions(new Date(0))
  );
}