95 lines
2.5 KiB
TypeScript
95 lines
2.5 KiB
TypeScript
import { redirect } from "@solidjs/router";
|
|
import { useSession } from "vinxi/http";
|
|
import { getRandomValues, subtle, timingSafeEqual } from "crypto";
|
|
import { createUser, findUser } from "./db";
|
|
import type { Language } from "~/i18n";
|
|
import { getTranslations } from "~/i18n";
|
|
|
|
export interface Session {
|
|
id: number;
|
|
email: string;
|
|
}
|
|
|
|
export const getSession = () =>
|
|
useSession<Session>({
|
|
password: process.env.SESSION_SECRET!,
|
|
});
|
|
|
|
export async function createSession(user: Session, redirectTo?: string) {
|
|
const validDest = redirectTo?.[0] === "/" && redirectTo[1] !== "/";
|
|
const session = await getSession();
|
|
await session.update(user);
|
|
return redirect(validDest ? redirectTo : "/");
|
|
}
|
|
|
|
async function createHash(password: string) {
|
|
const salt = getRandomValues(new Uint8Array(16));
|
|
const saltHex = Buffer.from(salt).toString("hex");
|
|
const key = await subtle.deriveBits(
|
|
{
|
|
name: "PBKDF2",
|
|
salt,
|
|
iterations: 100_000,
|
|
hash: "SHA-512",
|
|
},
|
|
await subtle.importKey(
|
|
"raw",
|
|
new TextEncoder().encode(password),
|
|
"PBKDF2",
|
|
false,
|
|
["deriveBits"],
|
|
),
|
|
512,
|
|
);
|
|
const hash = Buffer.from(key).toString("hex");
|
|
return `${saltHex}:${hash}`;
|
|
}
|
|
|
|
async function checkPassword(
|
|
storedPassword: string,
|
|
providedPassword: string,
|
|
lang: Language,
|
|
) {
|
|
const translations = getTranslations(lang);
|
|
const [storedSalt, storedHash] = storedPassword.split(":");
|
|
if (!storedSalt || !storedHash)
|
|
throw new Error(translations["errors.invalidStoredPasswordFormat"]);
|
|
const key = await subtle.deriveBits(
|
|
{
|
|
name: "PBKDF2",
|
|
salt: Buffer.from(storedSalt, "hex"),
|
|
iterations: 100_000,
|
|
hash: "SHA-512",
|
|
},
|
|
await subtle.importKey(
|
|
"raw",
|
|
new TextEncoder().encode(providedPassword),
|
|
"PBKDF2",
|
|
false,
|
|
["deriveBits"],
|
|
),
|
|
512,
|
|
);
|
|
const hash = Buffer.from(key);
|
|
const stored = Buffer.from(storedHash, "hex");
|
|
if (stored.length !== hash.length || !timingSafeEqual(stored, hash))
|
|
throw new Error(translations["errors.invalidEmailOrPassword"]);
|
|
}
|
|
|
|
export async function passwordLogin(
|
|
email: string,
|
|
password: string,
|
|
lang: Language = "en",
|
|
) {
|
|
const translations = getTranslations(lang);
|
|
let user = await findUser({ email });
|
|
if (!user)
|
|
user = await createUser({
|
|
email,
|
|
password: await createHash(password),
|
|
});
|
|
else if (!user.password) throw new Error(translations["errors.oauthOnly"]);
|
|
else await checkPassword(user.password, password, lang);
|
|
return createSession(user);
|
|
}
|