116 lines
2.9 KiB
TypeScript
116 lines
2.9 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);
|
|
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}"`,
|
|
);
|
|
} |