114 lines
2.4 KiB
TypeScript
114 lines
2.4 KiB
TypeScript
import fs from "fs";
|
|
import path from "path";
|
|
import bcrypt from "bcryptjs";
|
|
|
|
const DATA_DIR = path.join(process.cwd(), "data");
|
|
const USERS_FILE = path.join(DATA_DIR, "users.json");
|
|
|
|
export interface UserRow {
|
|
id: number;
|
|
username: string;
|
|
email: string;
|
|
password_hash: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface UserPublic {
|
|
id: number;
|
|
username: string;
|
|
email: string;
|
|
created_at: string;
|
|
}
|
|
|
|
function ensureDataDir() {
|
|
if (!fs.existsSync(DATA_DIR)) {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
function readUsers(): UserRow[] {
|
|
ensureDataDir();
|
|
if (!fs.existsSync(USERS_FILE)) {
|
|
return [];
|
|
}
|
|
try {
|
|
const raw = fs.readFileSync(USERS_FILE, "utf-8");
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function writeUsers(users: UserRow[]) {
|
|
ensureDataDir();
|
|
fs.writeFileSync(USERS_FILE, JSON.stringify(users, null, 2), "utf-8");
|
|
}
|
|
|
|
let nextId = 0;
|
|
function getNextId(): number {
|
|
if (nextId === 0) {
|
|
const users = readUsers();
|
|
nextId = users.reduce((max, u) => Math.max(max, u.id), 0) + 1;
|
|
}
|
|
return nextId++;
|
|
}
|
|
|
|
export function createUser(username: string, email: string, password: string): UserPublic {
|
|
const users = readUsers();
|
|
|
|
if (users.find((u) => u.email === email || u.username === username)) {
|
|
throw new Error("A user with that email or username already exists.");
|
|
}
|
|
|
|
const password_hash = bcrypt.hashSync(password, 10);
|
|
const newUser: UserRow = {
|
|
id: getNextId(),
|
|
username,
|
|
email,
|
|
password_hash,
|
|
created_at: new Date().toISOString(),
|
|
};
|
|
|
|
users.push(newUser);
|
|
writeUsers(users);
|
|
|
|
return {
|
|
id: newUser.id,
|
|
username: newUser.username,
|
|
email: newUser.email,
|
|
created_at: newUser.created_at,
|
|
};
|
|
}
|
|
|
|
export function authenticateUser(email: string, password: string): UserPublic {
|
|
const users = readUsers();
|
|
const user = users.find((u) => u.email === email);
|
|
|
|
if (!user) {
|
|
throw new Error("Invalid email or password.");
|
|
}
|
|
|
|
if (!bcrypt.compareSync(password, user.password_hash)) {
|
|
throw new Error("Invalid email or password.");
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
created_at: user.created_at,
|
|
};
|
|
}
|
|
|
|
export function getUserById(id: number): UserPublic | null {
|
|
const users = readUsers();
|
|
const user = users.find((u) => u.id === id);
|
|
if (!user) return null;
|
|
return {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
created_at: user.created_at,
|
|
};
|
|
}
|