v3: added user profile description

This commit is contained in:
khannurien
2026-03-22 21:07:17 +00:00
parent c5051e3485
commit d94a319d96
10 changed files with 227 additions and 26 deletions

View File

@@ -49,6 +49,7 @@ export interface UserRow {
created_at: string;
updated_at: string | null;
avatar_mime: string | null;
description: string | null;
invited_by: string | null;
// Present only when joined: LEFT JOIN users i ON i.id = u.invited_by
invited_by_username: string | null;
@@ -79,6 +80,7 @@ export function isDumpRow(obj: Record<string, SQLOutputValue>): obj is DumpRow {
"file_size" in obj &&
(typeof obj.file_size === "number" || obj.file_size === null) &&
"vote_count" in obj && typeof obj.vote_count === "number" &&
"comment_count" in obj && typeof obj.comment_count === "number" &&
"is_private" in obj && typeof obj.is_private === "number";
}
@@ -91,7 +93,9 @@ export function isUserRow(obj: Record<string, SQLOutputValue>): obj is UserRow {
"is_admin" in obj && typeof obj.is_admin === "number" &&
"created_at" in obj && typeof obj.created_at === "string" &&
"avatar_mime" in obj &&
(typeof obj.avatar_mime === "string" || obj.avatar_mime === null);
(typeof obj.avatar_mime === "string" || obj.avatar_mime === null) &&
"description" in obj &&
(typeof obj.description === "string" || obj.description === null);
}
/**
@@ -151,6 +155,7 @@ export function userRowToApi(row: UserRow): User {
createdAt: new Date(row.created_at),
updatedAt: row.updated_at ? new Date(row.updated_at) : undefined,
avatarMime: row.avatar_mime ?? undefined,
description: row.description ?? undefined,
invitedByUsername: typeof row.invited_by_username === "string"
? row.invited_by_username
: undefined,
@@ -166,6 +171,7 @@ export function userApiToRow(user: User): UserRow {
created_at: user.createdAt.toISOString(),
updated_at: user.updatedAt?.toISOString() ?? null,
avatar_mime: user.avatarMime ?? null,
description: user.description ?? null,
invited_by: null,
invited_by_username: null,
};

View File

@@ -44,6 +44,7 @@ export interface User {
createdAt: Date;
updatedAt?: Date;
avatarMime?: string;
description?: string;
invitedByUsername?: string;
}
@@ -62,6 +63,7 @@ export interface UpdateUserRequest {
username?: string;
password?: string;
isAdmin?: boolean;
description?: string | null;
}
export function isLoginUserRequest(obj: unknown): obj is LoginUserRequest {
@@ -83,7 +85,9 @@ export function isUpdateUserRequest(obj: unknown): obj is UpdateUserRequest {
return !!obj && typeof obj === "object" &&
(!("username" in obj) || typeof obj.username === "string") &&
(!("password" in obj) || typeof obj.password === "string") &&
(!("isAdmin" in obj) || typeof obj.isAdmin === "boolean");
(!("isAdmin" in obj) || typeof obj.isAdmin === "boolean") &&
(!("description" in obj) || typeof obj.description === "string" ||
obj.description === null);
}
export interface AuthResponse {

View File

@@ -5,6 +5,7 @@ import {
APIException,
isLoginUserRequest,
isRegisterUserRequest,
isUpdateUserRequest,
type PaginatedData,
} from "../model/interfaces.ts";
@@ -15,6 +16,7 @@ import {
getUserById,
getUserByUsername,
searchUsers,
updateUser,
} from "../services/user-service.ts";
import { redeemInvite, validateInvite } from "../services/invite-service.ts";
import {
@@ -132,6 +134,21 @@ router.get("/me", authMiddleware, (ctx: AuthContext) => {
}
});
// Update current user profile (description, etc.)
router.patch("/me", authMiddleware, async (ctx: AuthContext) => {
const body = await ctx.request.body.json();
if (!isUpdateUserRequest(body)) {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
400,
"Invalid request",
);
}
const updated = await updateUser(ctx.state.user.userId, body);
const { passwordHash: _, ...publicUser } = updated;
ctx.response.body = { success: true, data: publicUser };
});
// User search for @mention autocomplete
router.get("/search", (ctx) => {
const q = (ctx.request.url.searchParams.get("q") ?? "").trim();

View File

@@ -10,7 +10,7 @@ import { db, isUserRow, userApiToRow, userRowToApi } from "../model/db.ts";
import { hashPassword } from "../lib/jwt.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.invited_by,
`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,
i.username as invited_by_username
FROM users u
LEFT JOIN users i ON i.id = u.invited_by`;
@@ -118,24 +118,26 @@ export async function updateUser(
): Promise<User> {
const user = getUserById(userId);
const { password, ...requestFields } = request;
const { password, description, ...requestFields } = request;
const now = new Date();
const updatedUser: User = {
...user,
passwordHash: password ? await hashPassword(password) : user.passwordHash,
...requestFields,
description: description ?? user.description,
updatedAt: now,
};
const updatedUserRow = userApiToRow(updatedUser);
const userResult = db.prepare(
`UPDATE users SET username = ?, password_hash = ?, is_admin = ?, updated_at = ? WHERE id = ?`,
`UPDATE users SET username = ?, password_hash = ?, is_admin = ?, description = ?, updated_at = ? WHERE id = ?`,
).run(
updatedUserRow.username,
updatedUserRow.password_hash,
updatedUserRow.is_admin,
updatedUserRow.description,
now.toISOString(),
updatedUserRow.id,
);

View File

@@ -24,6 +24,7 @@ CREATE TABLE users (
created_at TEXT NOT NULL,
updated_at TEXT,
avatar_mime TEXT,
description TEXT,
invited_by TEXT REFERENCES users(id)
);