init app
This commit is contained in:
91
lib/auth.ts
Normal file
91
lib/auth.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import crypto from "node:crypto";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cache } from "react";
|
||||
import { prisma } from "./prisma";
|
||||
import { encryptSessionId, decryptSessionId } from "./crypto";
|
||||
|
||||
/**
|
||||
* Create a new session for the given user.
|
||||
* Sets an encrypted session cookie with a 7-day expiry.
|
||||
* Returns the encrypted cookie value.
|
||||
*/
|
||||
export async function createSession(userId: string): Promise<string> {
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
const session = await prisma.session.create({
|
||||
data: {
|
||||
sessionToken: token,
|
||||
userId,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
const encryptedValue = encryptSessionId(session.id);
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set("session", encryptedValue, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 7 * 24 * 60 * 60,
|
||||
});
|
||||
|
||||
return encryptedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current session cookie and its database record.
|
||||
*/
|
||||
export async function deleteSession(): Promise<void> {
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get("session")?.value;
|
||||
|
||||
if (sessionCookie) {
|
||||
const sessionId = decryptSessionId(sessionCookie);
|
||||
if (sessionId) {
|
||||
await prisma.session.delete({ where: { id: sessionId } }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
cookieStore.delete("session");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current authenticated user from the session cookie.
|
||||
* Memoized per-request with React cache().
|
||||
*/
|
||||
export const getCurrentUser = cache(
|
||||
async (): Promise<{ id: string; email: string } | null> => {
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get("session")?.value;
|
||||
|
||||
if (!sessionCookie) return null;
|
||||
|
||||
const sessionId = decryptSessionId(sessionCookie);
|
||||
if (!sessionId) return null;
|
||||
|
||||
const session = await prisma.session.findUnique({
|
||||
where: { id: sessionId },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (!session) return null;
|
||||
if (session.expiresAt < new Date()) return null;
|
||||
|
||||
return { id: session.user.id, email: session.user.email };
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the current user or redirect to /login if unauthenticated.
|
||||
*/
|
||||
export async function requireCurrentUser(): Promise<{
|
||||
id: string;
|
||||
email: string;
|
||||
}> {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) redirect("/login");
|
||||
return user;
|
||||
}
|
||||
115
lib/crypto.ts
Normal file
115
lib/crypto.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const PBKDF2_ITERATIONS = 100_000;
|
||||
const PBKDF2_KEYLEN = 32;
|
||||
const PBKDF2_DIGEST = "sha512";
|
||||
const SALT_BYTES = 32;
|
||||
|
||||
/**
|
||||
* Hash a password using pbkdf2 with a random salt.
|
||||
* Returns `salt:hash` in hex.
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const salt = crypto.randomBytes(SALT_BYTES).toString("hex");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
PBKDF2_ITERATIONS,
|
||||
PBKDF2_KEYLEN,
|
||||
PBKDF2_DIGEST,
|
||||
(err, derivedKey) => {
|
||||
if (err) return reject(err);
|
||||
resolve(`${salt}:${derivedKey.toString("hex")}`);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a password against a stored `salt:hash` string.
|
||||
* Uses timing-safe comparison.
|
||||
*/
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
stored: string,
|
||||
): Promise<boolean> {
|
||||
const [salt, originalHash] = stored.split(":");
|
||||
if (!salt || !originalHash) return false;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
PBKDF2_ITERATIONS,
|
||||
PBKDF2_KEYLEN,
|
||||
PBKDF2_DIGEST,
|
||||
(err, derivedKey) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
const hashBuf = Buffer.from(originalHash, "hex");
|
||||
if (hashBuf.length !== derivedKey.length) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(crypto.timingSafeEqual(hashBuf, derivedKey));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random hex token.
|
||||
*/
|
||||
export function generateToken(bytes: number = 32): string {
|
||||
return crypto.randomBytes(bytes).toString("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a session ID using AES-256-GCM.
|
||||
* SESSION_SECRET must be a hex-encoded 32-byte key.
|
||||
* Returns `iv:authTag:ciphertext` in hex.
|
||||
*/
|
||||
export function encryptSessionId(sessionId: string): string {
|
||||
const key = Buffer.from(process.env.SESSION_SECRET!, "hex");
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(sessionId, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a session ID encrypted with encryptSessionId.
|
||||
* Returns null on any error.
|
||||
*/
|
||||
export function decryptSessionId(encrypted: string): string | null {
|
||||
try {
|
||||
const key = Buffer.from(process.env.SESSION_SECRET!, "hex");
|
||||
const [ivHex, authTagHex, ciphertextHex] = encrypted.split(":");
|
||||
if (!ivHex || !authTagHex || !ciphertextHex) return null;
|
||||
|
||||
const iv = Buffer.from(ivHex, "hex");
|
||||
const authTag = Buffer.from(authTagHex, "hex");
|
||||
const ciphertext = Buffer.from(ciphertextHex, "hex");
|
||||
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(ciphertext),
|
||||
decipher.final(),
|
||||
]);
|
||||
|
||||
return decrypted.toString("utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
7
lib/prisma.ts
Normal file
7
lib/prisma.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
41
lib/types.ts
Normal file
41
lib/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export type OrderStatus = "pending" | "purchased" | "delivered" | "closed";
|
||||
|
||||
export interface OrderItemData {
|
||||
id: string;
|
||||
orderId: string;
|
||||
customerId: string | null;
|
||||
customerName: string | null;
|
||||
itemName: string;
|
||||
initPrice: number;
|
||||
myPrice: number;
|
||||
taxRatio: number;
|
||||
quantity: number;
|
||||
netPrice: number;
|
||||
myNetPrice: number;
|
||||
finalPrice: number;
|
||||
}
|
||||
|
||||
export interface OrderWithDetails {
|
||||
id: string;
|
||||
title: string;
|
||||
status: OrderStatus;
|
||||
notes: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
customers: { customer: { id: string; name: string } }[];
|
||||
items: OrderItemData[];
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
pending: number;
|
||||
purchased: number;
|
||||
delivered: number;
|
||||
closed: number;
|
||||
totalOrders: number;
|
||||
}
|
||||
|
||||
export interface FormState {
|
||||
errors?: Record<string, string[]>;
|
||||
message?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user