lifetracker/packages/trpc/auth.ts

129 lines
3.3 KiB
TypeScript

import { randomBytes } from "crypto";
import * as bcrypt from "bcryptjs";
import { db } from "@lifetracker/db";
import { apiKeys } from "@lifetracker/db/schema";
import serverConfig from "@lifetracker/shared/config";
import { authFailureLogger } from "@lifetracker/shared/logger";
// API Keys
const BCRYPT_SALT_ROUNDS = 10;
const API_KEY_PREFIX = "ak1";
export async function generateApiKey(name: string, userId: string) {
const id = randomBytes(10).toString("hex");
const secret = randomBytes(10).toString("hex");
const secretHash = await bcrypt.hash(secret, BCRYPT_SALT_ROUNDS);
const plain = `${API_KEY_PREFIX}_${id}_${secret}`;
const key = (
await db
.insert(apiKeys)
.values({
name: name,
userId: userId,
keyId: id,
keyHash: secretHash,
})
.returning()
)[0];
return {
id: key.id,
name: key.name,
createdAt: key.createdAt,
key: plain,
};
}
function parseApiKey(plain: string) {
const parts = plain.split("_");
if (parts.length != 3) {
throw new Error(
`Malformed API key. API keys should have 3 segments, found ${parts.length} instead.`,
);
}
if (parts[0] !== API_KEY_PREFIX) {
throw new Error(`Malformed API key. Got unexpected key prefix.`);
}
return {
keyId: parts[1],
keySecret: parts[2],
};
}
export async function authenticateApiKey(key: string) {
const { keyId, keySecret } = parseApiKey(key);
console.log("\n\nWELCOME TO HELL\n\n");
console.log(parseApiKey(key));
// Console.log all rows in the apiKeys table
console.log(await db.query.apiKeys.findMany());
console.log(await db.query.apiKeys.findFirst({
where: (k, { eq }) => eq(k.keyId, keyId),
with: {
user: true,
},
}));
console.log("\n\n\n\n");
const apiKey = await db.query.apiKeys.findFirst({
where: (k, { eq }) => eq(k.keyId, keyId),
with: {
user: true,
},
});
if (!apiKey) {
throw new Error("API key not found");
}
const hash = apiKey.keyHash;
const validation = await bcrypt.compare(keySecret, hash);
if (!validation) {
throw new Error("Invalid API Key");
}
return apiKey.user;
}
export async function hashPassword(password: string) {
return bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
}
export async function validatePassword(email: string, password: string) {
if (serverConfig.auth.disablePasswordAuth) {
throw new Error("Password authentication is currently disabled");
}
const user = await db.query.users.findFirst({
where: (u, { eq }) => eq(u.email, email),
});
if (!user) {
throw new Error("User not found");
}
if (!user.password) {
throw new Error("This user doesn't have a password defined");
}
const validation = await bcrypt.compare(password, user.password);
if (!validation) {
throw new Error("Wrong password");
}
return user;
}
export function logAuthenticationError(
user: string,
message: string,
ip: string | null,
): void {
authFailureLogger.error(
`Authentication error. User: "${user}", Message: "${message}", IP-Address: "${ip}"`,
);
}