Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions apps/api/src/lib/db.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createDb, type DB } from "@cnode/db";
import { createDb, type Database, type DB } from "@cnode/db";
import {
users,
topics,
Expand All @@ -21,7 +21,7 @@ import { boolEq, boolValue } from "./db-compat";
import { deleteReplyWithStore, type ReplyDeletionStore } from "./reply-deletion";
import { createReplyWithStore, type ReplyCreationStore } from "./reply-creation";

let dbInstance: DB = null;
let dbInstance: DB | null = null;
const INTERNAL_TABS = ["dev", "test"];

function getDb(): DB {
Expand Down Expand Up @@ -121,7 +121,7 @@ export const userQueries = {

async clearGithubInfo(userId: number, githubId: string) {
const db = getDb();
return db.transaction(async (tx: DB) => {
return db.transaction(async (tx) => {
const [updated] = await tx
.update(users)
.set({ githubId: null, githubUsername: null, githubAccessToken: null })
Expand Down Expand Up @@ -379,7 +379,7 @@ export const topicQueries = {
},
};

export function buildTopicsByQuery(db: DB, where: any, opt?: any) {
export function buildTopicsByQuery(db: Database, where: any, opt?: any) {
let q = db.select().from(topics).$dynamic();
const conditions = topicConditions(where);
if (conditions.length > 0) {
Expand Down Expand Up @@ -424,7 +424,7 @@ export const replyQueries = {
const db = getDb();
const store: ReplyCreationStore = {
transaction: (callback) =>
db.transaction(async (tx: DB) =>
db.transaction(async (tx) =>
callback({
async lockTopic(id) {
const rows = await tx
Expand All @@ -441,7 +441,12 @@ export const replyQueries = {
.insert(replies)
.values({ ...input, createAt: now, updateAt: now })
.returning();
return reply;
return {
id: reply.id,
topicId: reply.topicId,
authorId: reply.authorId,
createAt: reply.createAt ?? now,
};
},
async incrementAuthor(id) {
await tx
Expand Down Expand Up @@ -501,7 +506,7 @@ export const replyQueries = {
const db = getDb();
const store: ReplyDeletionStore = {
transaction: (callback) =>
db.transaction(async (tx: DB) =>
db.transaction(async (tx) =>
callback({
async lockReply(id) {
const rows = await tx
Expand Down Expand Up @@ -591,7 +596,7 @@ export const replyQueries = {
},
};

export function buildRepliesByTopicQuery(db: DB, topicId: number) {
export function buildRepliesByTopicQuery(db: Database, topicId: number) {
return db
.select()
.from(replies)
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/lib/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ export async function getMessageRelations(msg: typeof messages.$inferSelect) {
msg.topicId ? topicQueries.getById(msg.topicId) : null,
msg.replyId ? replyQueries.getById(msg.replyId) : null,
]);

return {
...msg,
author,
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { roleQueries, userQueries } from "../lib/db";
import { resolveUserAccess } from "../lib/user-access";

export interface AuthVars {
user: Awaited<ReturnType<typeof userQueries.getById>>;
user: Awaited<ReturnType<typeof userQueries.getById>> | null;
isLogin: boolean;
isAdmin: boolean;
isMod: boolean;
Expand Down
14 changes: 7 additions & 7 deletions apps/api/src/routes/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ async function getReportTargetSummary(targetType: string, targetId: number) {
target_id: String(reply.id),
topic_id: String(topic.id),
title: topic.title,
summary: reply.content.slice(0, 160),
summary: (reply.content ?? "").slice(0, 160),
};
}
return null;
Expand Down Expand Up @@ -693,7 +693,7 @@ admin.post("/admin/topics/permanent-delete", adminRequired(), async (c) => {
if (!result) continue;
results.push({
topic_id: id,
title: result.topic.title,
title: result.topic.title ?? "",
deleted_replies: result.deletedReplies,
});
}
Expand Down Expand Up @@ -763,7 +763,7 @@ admin.post("/topic/:tid/top", modRequired(), async (c) => {
user.id,
user.loginname,
topic.top ? "untop" : "top",
{ type: "topic", id: String(tid), name: topic.title },
{ type: "topic", id: String(tid), name: topic.title ?? undefined },
"success",
);
return c.json({ success: true, message: topic.top ? "已取消置顶" : "已置顶" });
Expand All @@ -783,7 +783,7 @@ admin.post("/topic/:tid/good", modRequired(), async (c) => {
user.id,
user.loginname,
topic.good ? "ungood" : "good",
{ type: "topic", id: String(tid), name: topic.title },
{ type: "topic", id: String(tid), name: topic.title ?? undefined },
"success",
);
return c.json({ success: true, message: topic.good ? "已取消加精" : "已加精" });
Expand All @@ -803,7 +803,7 @@ admin.post("/topic/:tid/lock", modRequired(), async (c) => {
user.id,
user.loginname,
topic.lock ? "unlock" : "lock",
{ type: "topic", id: String(tid), name: topic.title },
{ type: "topic", id: String(tid), name: topic.title ?? undefined },
"success",
);
return c.json({ success: true, message: topic.lock ? "已解锁" : "已锁定" });
Expand All @@ -830,7 +830,7 @@ admin.post("/topic/:tid/delete", async (c) => {
user.id,
user.loginname,
"delete_topic",
{ type: "topic", id: String(tid), name: topic.title },
{ type: "topic", id: String(tid), name: topic.title ?? undefined },
"success",
);
return c.json({ success: true, message: "话题已删除" });
Expand Down Expand Up @@ -1361,7 +1361,7 @@ admin.openapi(createReportRoute, async (c) => {
user.id,
user.loginname,
"report_auto_hide",
{ type: targetType, id: String(tid), name: summary.title },
{ type: targetType, id: String(tid), name: summary.title ?? undefined },
"success",
JSON.stringify({ reporter_count: Number(reporterCount), threshold }),
);
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ auth.openapi(loginRoute, async (c) => {
let user = await userQueries.getByLoginName(name.toLowerCase());
if (!user) user = await userQueries.getByEmail(name);
if (!user) return c.json({ success: false as const, error_msg: "用户名或密码错误" }, 403);
const equal = await bcryptjs.compare(pass, user.pass);
const equal = user.pass ? await bcryptjs.compare(pass, user.pass) : false;
if (!equal) return c.json({ success: false as const, error_msg: "用户名或密码错误" }, 403);
if (!user.active) return c.json({ success: false as const, error_msg: "账号未激活" }, 403);
setSessionCookie(c, user.id);
Expand Down Expand Up @@ -572,7 +572,7 @@ auth.openapi(githubCreateRoute, async (c) => {
if (!profile)
return c.json({ success: false as const, error_msg: "GitHub 登录状态已过期,请重新授权" }, 401);
const body = c.req.valid("json");
let user: Awaited<ReturnType<typeof userQueries.getById>> = null;
let user: Awaited<ReturnType<typeof userQueries.getById>> | null = null;
if (body.isnew) {
const loginname = profile.login.toLowerCase();
if (await userQueries.getByLoginName(loginname))
Expand Down Expand Up @@ -910,7 +910,7 @@ auth.openapi(changePassRoute, async (c) => {
const user = c.get("user");
if (!user) return c.json({ success: false as const, error_msg: "未登录" }, 401);
const { oldPass, newPass } = c.req.valid("json");
if (!(await bcryptjs.compare(oldPass, user.pass)))
if (!user.pass || !(await bcryptjs.compare(oldPass, user.pass)))
return c.json({ success: false as const, error_msg: "原密码错误" }, 403);
if (newPass.length < 8 || !/[a-zA-Z]/.test(newPass) || !/[0-9]/.test(newPass))
return c.json(
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/routes/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ collect.openapi(listCollectRoute, async (c) => {
visit_count: t.visitCount,
create_at: t.createAt,
author: author
? { loginname: author.loginname, avatar_url: author.avatar }
? { loginname: author.loginname, avatar_url: author.avatar ?? "" }
: { loginname: "", avatar_url: "" },
};
}),
Expand Down
41 changes: 18 additions & 23 deletions apps/api/src/routes/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,45 +61,40 @@ message.openapi(listMessagesRoute, async (c) => {
getReadMessagesByUserId(currentUser.id),
getUnreadMessagesByUserId(currentUser.id),
]);
const formatMessage = async (msg: any) => {
const formatMessage = async (msg: (typeof readMsgs)[number]) => {
const relations = await getMessageRelations(msg);
if (!relations.author?.loginname || !relations.topic) return null;
const { author, topic } = relations;
return {
id: String(relations.id),
type: relations.type,
// Legacy rows migrated from Mongo may carry other type values; they pass
// through on the wire unchanged, matching the previous behavior.
type: relations.type as "at" | "reply" | "reply2",
has_read: !!relations.hasRead,
create_at: relations.createAt,
author: relations.author
? { loginname: relations.author.loginname, avatar_url: relations.author.avatar }
: { loginname: "", avatar_url: "" },
topic: relations.topic
? {
id: String(relations.topic.id),
author: relations.topic.author
? {
loginname: relations.topic.author.loginname,
avatar_url: relations.topic.author.avatar,
}
: { loginname: "", avatar_url: "" },
title: relations.topic.title,
last_reply_at: relations.topic.lastReplyAt,
}
: null,
create_at: relations.createAt?.toISOString() ?? "",
author: { loginname: author.loginname, avatar_url: author.avatar ?? "" },
topic: {
id: String(topic.id),
author: { loginname: "", avatar_url: "" },
title: topic.title ?? "",
last_reply_at: topic.lastReplyAt ? topic.lastReplyAt.toISOString() : null,
},
reply: relations.reply
? {
id: String(relations.reply.id),
content: renderMarkdown(relations.reply.content, mdrender),
ups: [],
create_at: relations.reply.createAt,
create_at: relations.reply.createAt?.toISOString() ?? "",
}
: {},
: { id: "", content: "", ups: [], create_at: "" },
};
};
const [hasReadRaw, hasUnreadRaw] = await Promise.all([
Promise.all(readMsgs.map(formatMessage)),
Promise.all(unreadMsgs.map(formatMessage)),
]);
const hasRead = hasReadRaw.filter((msg: any) => msg.author?.loginname && msg.topic);
const hasUnread = hasUnreadRaw.filter((msg: any) => msg.author?.loginname && msg.topic);
const hasRead = hasReadRaw.filter((msg) => msg !== null);
const hasUnread = hasUnreadRaw.filter((msg) => msg !== null);
return c.json(
{
success: true as const,
Expand Down
12 changes: 6 additions & 6 deletions apps/api/src/routes/reply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ const createReplyRoute = createRoute({
});

reply.openapi(createReplyRoute, async (c) => {
let user = c.get("user");
if (!user) {
const sessionUser = c.get("user");
if (!sessionUser) {
return c.json({ success: false as const, error_msg: "未登录" }, 401);
}
user = await ensureMuteNotExpired(user);
const user: NonNullable<AuthVars["user"]> = await ensureMuteNotExpired(sessionUser);
if (user.isMuted || user.isBlock) {
return c.json({ success: false as const, error_msg: "您已被禁言" }, 403);
}
Expand Down Expand Up @@ -200,9 +200,9 @@ reply.openapi(getReplyRoute, async (c) => {
data: {
id: String(replyData.id),
topic_id: String(replyData.topicId),
content: replyData.content,
create_at: replyData.createAt,
update_at: replyData.updateAt,
content: replyData.content ?? "",
create_at: replyData.createAt?.toISOString() ?? "",
update_at: replyData.updateAt ? replyData.updateAt.toISOString() : null,
},
},
200,
Expand Down
21 changes: 10 additions & 11 deletions apps/api/src/routes/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
createTopicBodySchema,
updateTopicBodySchema,
errorResponseSchema,
type TopicDTO,
} from "@cnode/shared";
import { z } from "zod";

Expand Down Expand Up @@ -138,17 +137,17 @@ topic.openapi(listTopicsRoute, async (c) => {
return {
id: String(t.id),
author_id: String(t.authorId),
tab: t.tab,
tab: t.tab ?? "",
content: renderMarkdown(t.content, mdrender),
title: t.title,
last_reply_at: t.lastReplyAt,
title: t.title ?? "",
last_reply_at: t.lastReplyAt ? t.lastReplyAt.toISOString() : null,
good: !!t.good,
top: !!t.top,
reply_count: t.replyCount,
visit_count: t.visitCount,
create_at: t.createAt,
reply_count: t.replyCount ?? 0,
visit_count: t.visitCount ?? 0,
create_at: t.createAt?.toISOString() ?? "",
author: userSummary(author),
} as TopicDTO;
};
}),
);

Expand Down Expand Up @@ -336,11 +335,11 @@ const createTopicRoute = createRoute({
});

topic.openapi(createTopicRoute, async (c) => {
let user = c.get("user");
if (!user) {
const sessionUser = c.get("user");
if (!sessionUser) {
return c.json({ success: false as const, error_msg: "未登录" }, 401);
}
user = await ensureMuteNotExpired(user);
const user: NonNullable<AuthVars["user"]> = await ensureMuteNotExpired(sessionUser);
if (user.isMuted || user.isBlock) {
return c.json({ success: false as const, error_msg: "您已被禁言" }, 403);
}
Expand Down
10 changes: 5 additions & 5 deletions apps/api/src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async function formatTopic(t: any) {
visit_count: t.visitCount,
create_at: t.createAt,
author: author
? { loginname: author.loginname, avatar_url: author.avatar }
? { loginname: author.loginname, avatar_url: author.avatar ?? "" }
: { loginname: "", avatar_url: "" },
};
}
Expand Down Expand Up @@ -374,7 +374,7 @@ user.openapi(userDetailRoute, async (c) => {
}
const fmtTopic = (t: any) => ({
id: String(t.id),
author: { loginname: userData.loginname, avatar_url: userData.avatar },
author: { loginname: userData.loginname, avatar_url: userData.avatar ?? "" },
title: t.title,
last_reply_at: t.lastReplyAt,
});
Expand All @@ -384,13 +384,13 @@ user.openapi(userDetailRoute, async (c) => {
success: true as const,
data: {
loginname: userData.loginname,
avatar_url: userData.avatar,
avatar_url: userData.avatar ?? "",
githubUsername: userData.githubUsername?.trim() || "",
location: userData.location?.trim() || null,
url: userData.url?.trim() || null,
signature: userData.signature?.trim() || null,
identities,
create_at: userData.createAt,
create_at: userData.createAt?.toISOString() ?? "",
score: userData.score || 0,
topic_count: userData.topicCount || 0,
reply_count: userData.replyCount || 0,
Expand Down Expand Up @@ -450,7 +450,7 @@ user.openapi(accessTokenRoute, async (c) => {
{
success: true as const,
loginname: userData.loginname,
avatar_url: userData.avatar,
avatar_url: userData.avatar ?? "",
id: String(userData.id),
is_block: !!userData.isBlock,
is_muted: !!userData.isMuted || !!userData.isBlock,
Expand Down
2 changes: 1 addition & 1 deletion apps/api/test/reply-data-consistency.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vite-plus/test";
import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "@cnode/db";
import { schema } from "@cnode/db";
import { buildRepliesByTopicQuery, buildTopicsByQuery } from "../src/lib/db";
import {
deleteReplyWithStore,
Expand Down
Loading