Skip to content
Draft
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
71 changes: 62 additions & 9 deletions convex/comments.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ConvexError, v } from 'convex/values';
import { mutation, query } from './_generated/server.js';
import { limiter } from './rateLimits.js';
import { assertAdmin, isAdmin } from './lib/auth.js';
import { assertAdmin, assertServer, isAdmin } from './lib/auth.js';
import { publicComment } from './lib/serialize.js';

const MAX_DEPTH = 2;
const MAX_COMMENTS = 200;
const MAX_USERNAME_LENGTH = 32;
const MAX_TEXT_LENGTH = 200;

async function isBanned(ctx, ipHash) {
const ban = await ctx.db
Expand Down Expand Up @@ -63,8 +65,9 @@ export const list = query({
});

export const getForAuth = query({
args: { commentId: v.id('comments') },
handler: async (ctx, { commentId }) => {
args: { commentId: v.id('comments'), serverSecret: v.string() },
handler: async (ctx, { commentId, serverSecret }) => {
assertServer(serverSecret);
const doc = await ctx.db.get(commentId);
if (!doc) return null;
return { passwordHash: doc.passwordHash, deletedAt: doc.deletedAt };
Expand All @@ -79,12 +82,24 @@ export const create = mutation({
text: v.string(),
parentId: v.optional(v.id('comments')),
ipHash: v.string(),
serverSecret: v.string(),
adminSecret: v.optional(v.string())
},
handler: async (ctx, args) => {
assertServer(args.serverSecret);
const admin = isAdmin(args.adminSecret);
const username = args.username.trim() || 'Anonymous';
const text = args.text.trim();

if (await isBanned(ctx, args.ipHash)) {
if (
username.length > MAX_USERNAME_LENGTH ||
text.length === 0 ||
text.length > MAX_TEXT_LENGTH
) {
throw new ConvexError({ kind: 'BadRequest', message: 'Invalid comment' });
}

if (!admin && (await isBanned(ctx, args.ipHash))) {
throw new ConvexError({ kind: 'Banned' });
}

Expand All @@ -100,15 +115,21 @@ export const create = mutation({
if (parent.depth >= MAX_DEPTH) {
throw new ConvexError({ kind: 'BadRequest', message: 'Maximum reply depth reached' });
}
if (parent.url !== args.url) {
throw new ConvexError({
kind: 'BadRequest',
message: 'Parent comment belongs to another page'
});
}
depth = parent.depth + 1;
parentId = args.parentId;
}

const id = await ctx.db.insert('comments', {
url: args.url,
username: args.username.trim() || 'Anonymous',
username,
passwordHash: args.passwordHash,
text: args.text,
text,
ipHash: args.ipHash,
parentId,
depth,
Expand All @@ -127,12 +148,14 @@ export const vote = mutation({
commentId: v.id('comments'),
voteType: v.union(v.literal('up'), v.literal('down')),
ipHash: v.string(),
serverSecret: v.string(),
adminSecret: v.optional(v.string())
},
handler: async (ctx, args) => {
assertServer(args.serverSecret);
const admin = isAdmin(args.adminSecret);

if (await isBanned(ctx, args.ipHash)) {
if (!admin && (await isBanned(ctx, args.ipHash))) {
throw new ConvexError({ kind: 'Banned' });
}

Expand Down Expand Up @@ -172,10 +195,20 @@ export const applyEdit = mutation({
commentId: v.id('comments'),
text: v.string(),
ipHash: v.string(),
serverSecret: v.string(),
adminSecret: v.optional(v.string())
},
handler: async (ctx, args) => {
assertServer(args.serverSecret);
const admin = isAdmin(args.adminSecret);
const text = args.text.trim();

if (text.length === 0 || text.length > MAX_TEXT_LENGTH) {
throw new ConvexError({ kind: 'BadRequest', message: 'Invalid comment' });
}
if (!admin && (await isBanned(ctx, args.ipHash))) {
throw new ConvexError({ kind: 'Banned' });
}
if (!admin) await consumeRateLimit(ctx, 'edit', args.ipHash);

const comment = await ctx.db.get(args.commentId);
Expand All @@ -184,8 +217,28 @@ export const applyEdit = mutation({
}

const updatedAt = Date.now();
await ctx.db.patch(args.commentId, { text: args.text, updatedAt });
return { id: args.commentId, text: args.text, updatedAt };
await ctx.db.patch(args.commentId, { text, updatedAt });
return { id: args.commentId, text, updatedAt };
}
});

export const softDelete = mutation({
args: {
commentId: v.id('comments'),
serverSecret: v.string()
},
handler: async (ctx, { commentId, serverSecret }) => {
assertServer(serverSecret);
const comment = await ctx.db.get(commentId);
if (!comment || comment.deletedAt !== null) {
throw new ConvexError({ kind: 'NotFound', message: 'Comment not found' });
}

await ctx.db.patch(commentId, {
username: '[deleted]',
text: '[deleted]',
updatedAt: Date.now()
});
}
});

Expand Down
6 changes: 6 additions & 0 deletions convex/lib/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ export function isAdmin(secret) {
return Boolean(process.env.ADMIN_SECRET) && secret === process.env.ADMIN_SECRET;
}

export function assertServer(secret) {
if (!isAdmin(secret)) {
throw new ConvexError({ kind: 'Unauthorized' });
}
}

export function assertAdmin(secret) {
if (!isAdmin(secret)) {
throw new ConvexError({ kind: 'Unauthorized' });
Expand Down
6 changes: 4 additions & 2 deletions convex/likes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ConvexError, v } from 'convex/values';
import { mutation, query } from './_generated/server.js';
import { limiter } from './rateLimits.js';
import { isAdmin } from './lib/auth.js';
import { assertServer, isAdmin } from './lib/auth.js';

async function isBanned(ctx, ipHash) {
const ban = await ctx.db
Expand Down Expand Up @@ -31,12 +31,14 @@ export const toggle = mutation({
args: {
url: v.string(),
ipHash: v.string(),
serverSecret: v.string(),
adminSecret: v.optional(v.string())
},
handler: async (ctx, args) => {
assertServer(args.serverSecret);
const admin = isAdmin(args.adminSecret);

if (await isBanned(ctx, args.ipHash)) {
if (!admin && (await isBanned(ctx, args.ipHash))) {
throw new ConvexError({ kind: 'Banned' });
}

Expand Down
13 changes: 11 additions & 2 deletions convex/now.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { query, mutation } from './_generated/server';
import { v } from 'convex/values';
import { ConvexError, v } from 'convex/values';
import { assertAdmin } from './lib/auth.js';

const MAX_CONTENT_LENGTH = 20000;

export const get = query({
args: {},
Expand All @@ -11,9 +14,15 @@ export const get = query({

export const update = mutation({
args: {
content: v.string()
content: v.string(),
adminSecret: v.string()
},
handler: async (ctx, args) => {
assertAdmin(args.adminSecret);
if (args.content.length > MAX_CONTENT_LENGTH) {
throw new ConvexError({ kind: 'BadRequest', message: 'Now page content is too long' });
}

const docs = await ctx.db.query('nowPage').take(1);
if (docs.length > 0) {
await ctx.db.patch(docs[0]._id, {
Expand Down
20 changes: 13 additions & 7 deletions mdsvex.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ const config = {
highlight: {
highlighter: createHighlighter({
// keepBackground: false,
theme: 'github-dark'
theme: {
light: 'github-light',
dark: 'github-dark'
}
})
},
smartypants: {
Expand All @@ -28,13 +31,16 @@ const config = {
remarkMath,
remarkGfm,
remarkGemoji,
[remarkEmbedder.default, {
transformers: [oembedTransformer.default],
handleError: ({ error, url }) => {
console.error(`Failed to embed ${url}:`, error);
return `<a href="${url}">${url}</a>`;
[
remarkEmbedder.default,
{
transformers: [oembedTransformer.default],
handleError: ({ error, url }) => {
console.error(`Failed to embed ${url}:`, error);
return `<a href="${url}">${url}</a>`;
}
}
}]
]
// [enhancedImage, { attributes: { loading: 'lazy', fetchpriority: 'low' } }]
],
rehypePlugins: [
Expand Down
Loading