239 lines
6.1 KiB
TypeScript
239 lines
6.1 KiB
TypeScript
import {
|
|
APIErrorCode,
|
|
APIException,
|
|
type RegisterUserRequest,
|
|
type UpdateUserRequest,
|
|
type User,
|
|
} from "../model/interfaces.ts";
|
|
import { db, isUserRow, userApiToRow, userRowToApi } from "../model/db.ts";
|
|
import { disconnectUser } from "./ws-service.ts";
|
|
|
|
import { hashPassword } from "../lib/jwt.ts";
|
|
import { linkAttachments } from "./attachment-service.ts";
|
|
|
|
const USER_SELECT =
|
|
`SELECT u.id, u.username, u.password_hash, u.is_admin, u.created_at, u.updated_at, u.avatar_mime, u.description, u.invited_by, u.email,
|
|
i.username as invited_by_username
|
|
FROM users u
|
|
LEFT JOIN users i ON i.id = u.invited_by`;
|
|
|
|
export async function createUser(
|
|
request: RegisterUserRequest,
|
|
inviterId: string | null,
|
|
): Promise<User> {
|
|
const userId = crypto.randomUUID();
|
|
const createdAt = new Date();
|
|
|
|
const existingUser = db.prepare(
|
|
"SELECT id FROM users WHERE username = ?;",
|
|
).get(request.username);
|
|
|
|
if (existingUser) {
|
|
throw new APIException(
|
|
APIErrorCode.VALIDATION_ERROR,
|
|
400,
|
|
"Username already exists",
|
|
);
|
|
}
|
|
|
|
const passwordHash = await hashPassword(request.password);
|
|
|
|
db.prepare(
|
|
`INSERT INTO users (id, username, password_hash, is_admin, created_at, invited_by, email)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?);`,
|
|
).run(
|
|
userId,
|
|
request.username,
|
|
passwordHash,
|
|
0,
|
|
createdAt.toISOString(),
|
|
inviterId,
|
|
request.email,
|
|
);
|
|
|
|
return {
|
|
id: userId,
|
|
username: request.username,
|
|
passwordHash,
|
|
isAdmin: false,
|
|
createdAt,
|
|
email: request.email,
|
|
};
|
|
}
|
|
|
|
export function getUserById(userId: string): User {
|
|
const userRow = db.prepare(
|
|
`${USER_SELECT} WHERE u.id = ?`,
|
|
).get(userId);
|
|
|
|
if (!userRow || !isUserRow(userRow)) {
|
|
throw new APIException(APIErrorCode.NOT_FOUND, 404, "User not found");
|
|
}
|
|
|
|
return userRowToApi(userRow);
|
|
}
|
|
|
|
export function getUserByUsername(username: string): User {
|
|
const userRow = db.prepare(
|
|
`${USER_SELECT} WHERE u.username = ?`,
|
|
).get(username);
|
|
|
|
if (!userRow || !isUserRow(userRow)) {
|
|
throw new APIException(APIErrorCode.NOT_FOUND, 404, "User not found");
|
|
}
|
|
|
|
return userRowToApi(userRow);
|
|
}
|
|
|
|
export function getUserByEmail(email: string): User | null {
|
|
const userRow = db.prepare(
|
|
`${USER_SELECT} WHERE u.email = ?`,
|
|
).get(email);
|
|
|
|
if (!userRow || !isUserRow(userRow)) return null;
|
|
return userRowToApi(userRow);
|
|
}
|
|
|
|
export function searchUsers(
|
|
query: string,
|
|
limit: number,
|
|
): { id: string; username: string; avatarMime: string | null }[] {
|
|
if (!query) return [];
|
|
const rows = db.prepare(
|
|
`SELECT id, username, avatar_mime FROM users WHERE username LIKE ? ORDER BY username LIMIT ?;`,
|
|
).all(`${query}%`, limit) as {
|
|
id: string;
|
|
username: string;
|
|
avatar_mime: string | null;
|
|
}[];
|
|
return rows.map((r) => ({
|
|
id: r.id,
|
|
username: r.username,
|
|
avatarMime: r.avatar_mime,
|
|
}));
|
|
}
|
|
|
|
export function searchUsersGlobal(query: string, limit: number): User[] {
|
|
if (!query.trim()) return [];
|
|
const pattern = `%${query}%`;
|
|
const rows = db.prepare(
|
|
`${USER_SELECT} WHERE (u.username LIKE ? OR u.description LIKE ?) ORDER BY u.username LIMIT ?;`,
|
|
).all(pattern, pattern, limit);
|
|
return rows.filter(isUserRow).map(userRowToApi);
|
|
}
|
|
|
|
export function listUsers(): User[] {
|
|
const userRows = db.prepare(
|
|
`${USER_SELECT}`,
|
|
).all();
|
|
|
|
if (!userRows || !userRows.every(isUserRow)) {
|
|
throw new APIException(APIErrorCode.NOT_FOUND, 404, "No user found");
|
|
}
|
|
|
|
return userRows.map(userRowToApi);
|
|
}
|
|
|
|
export async function updateUser(
|
|
userId: string,
|
|
request: UpdateUserRequest,
|
|
): Promise<User> {
|
|
const user = getUserById(userId);
|
|
|
|
const { password, description, email, ...requestFields } = request;
|
|
|
|
const now = new Date();
|
|
const updatedUser: User = {
|
|
...user,
|
|
passwordHash: password ? await hashPassword(password) : user.passwordHash,
|
|
...requestFields,
|
|
description: description ?? user.description,
|
|
email: email ?? user.email,
|
|
updatedAt: now,
|
|
};
|
|
|
|
const updatedUserRow = userApiToRow(updatedUser);
|
|
|
|
const userResult = db.prepare(
|
|
`UPDATE users SET username = ?, password_hash = ?, is_admin = ?, description = ?, email = ?, updated_at = ? WHERE id = ?`,
|
|
).run(
|
|
updatedUserRow.username,
|
|
updatedUserRow.password_hash,
|
|
updatedUserRow.is_admin,
|
|
updatedUserRow.description,
|
|
updatedUserRow.email,
|
|
now.toISOString(),
|
|
updatedUserRow.id,
|
|
);
|
|
|
|
if (userResult.changes === 0) {
|
|
throw new APIException(APIErrorCode.NOT_FOUND, 404, "Dump not found");
|
|
}
|
|
|
|
if (updatedUser.description) {
|
|
linkAttachments(updatedUser.description, userId);
|
|
}
|
|
|
|
return updatedUser;
|
|
}
|
|
|
|
export function updateUserAvatar(userId: string, mime: string): void {
|
|
const result = db.prepare(
|
|
`UPDATE users SET avatar_mime = ?, updated_at = ? WHERE id = ?`,
|
|
).run(mime, new Date().toISOString(), userId);
|
|
|
|
if (result.changes === 0) {
|
|
throw new APIException(APIErrorCode.NOT_FOUND, 404, "User not found");
|
|
}
|
|
}
|
|
|
|
export function getInviteTree(
|
|
userId: string,
|
|
): {
|
|
id: string;
|
|
username: string;
|
|
avatarMime?: string;
|
|
invitedById: string;
|
|
createdAt: Date;
|
|
}[] {
|
|
const rows = db.prepare(`
|
|
WITH RECURSIVE tree AS (
|
|
SELECT id, username, avatar_mime, invited_by, created_at, 0 AS depth
|
|
FROM users
|
|
WHERE invited_by = ?
|
|
UNION ALL
|
|
SELECT u.id, u.username, u.avatar_mime, u.invited_by, u.created_at, t.depth + 1
|
|
FROM users u
|
|
INNER JOIN tree t ON u.invited_by = t.id
|
|
WHERE t.depth < 10
|
|
)
|
|
SELECT * FROM tree ORDER BY created_at;
|
|
`).all(userId) as {
|
|
id: string;
|
|
username: string;
|
|
avatar_mime: string | null;
|
|
invited_by: string;
|
|
created_at: string;
|
|
}[];
|
|
|
|
return rows.map((r) => ({
|
|
id: r.id,
|
|
username: r.username,
|
|
avatarMime: r.avatar_mime ?? undefined,
|
|
invitedById: r.invited_by,
|
|
createdAt: new Date(r.created_at),
|
|
}));
|
|
}
|
|
|
|
export function deleteUser(userId: string): void {
|
|
disconnectUser(userId);
|
|
|
|
const result = db.prepare(
|
|
`DELETE FROM users WHERE id = ?;`,
|
|
).run(userId);
|
|
|
|
if (result.changes === 0) {
|
|
throw new APIException(APIErrorCode.NOT_FOUND, 404, "User not found");
|
|
}
|
|
}
|