Merge pull request #2 from 2alf/main

[1] Stored XSS fix, [2] X-Frame add, [3] Cron job to remove clustering
This commit is contained in:
João Donato
2026-01-04 20:07:38 +00:00
committed by GitHub
6 changed files with 78 additions and 2 deletions

2
.gitignore vendored
View File

@@ -86,7 +86,7 @@ jspm_packages/
# Gatsby files
.cache/
public
# Storybook build outputs
.out

12
convex/crons.ts Normal file
View File

@@ -0,0 +1,12 @@
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
crons.daily(
"delete closed requests after a day",
{ hourUTC: 1, minuteUTC: 0 },
internal.requests.deleteOldClosedRequests
);
export default crons;

View File

@@ -236,6 +236,13 @@ export const insertLeak = mutation({
};
}
if (args.url && !/^https?:\/\/.+/i.test(args.url)) {
return {
success: false as const,
error: "Invalid URL.",
};
}
// Validate required fields
if (!args.targetName || !args.provider || !args.leakText) {
return {
@@ -537,6 +544,7 @@ export const applyConsensusResult = internalMutation({
await ctx.db.patch(args.requestId, {
closed: true,
closedBy: "verification",
});
const submitter = await ctx.db.get(leak.submittedBy);

View File

@@ -1,8 +1,13 @@
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { query, mutation } from "./_generated/server";
import { query, mutation, internalMutation } from "./_generated/server";
import { Id } from "./_generated/dataModel";
// url validation helper
function isValidUrl(url: string): boolean {
return /^https?:\/\/.+/i.test(url); // only http and https
}
/**
* Get all open (non-closed) requests from the database.
* Returns requests with submitter names populated.
@@ -104,6 +109,7 @@ export const getUserOpenRequests = query({
},
});
/**
* Create a new request for a leak target.
* Validates that:
@@ -147,6 +153,13 @@ export const createRequest = mutation({
};
}
if (!isValidUrl(args.targetUrl)) {
return {
success: false as const,
error: "The provided target URL is not valid. Please provide a valid URL starting with http:// or https://",
};
}
// Check if a request with the same target name already exists (case-insensitive)
// Query ALL requests, not just open ones
const allRequests = await ctx.db.query("requests").collect();
@@ -264,6 +277,7 @@ export const closeRequest = mutation({
// Close the request
await ctx.db.patch(args.requestId, {
closed: true,
closedBy: "user",
});
return { success: true as const };
@@ -439,3 +453,41 @@ export const getRequestsWithVerificationStatus = query({
return requestsWithStatus;
},
});
/*
Delete closed requests after a day --> internal mutation called by a cron job
*/
export const deleteOldClosedRequests = internalMutation({
args: {},
returns: v.number(),
handler: async (ctx) => {
//const daysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
const daysAgo = Date.now() - 24 * 60 * 60 * 1000; // 1 day in milliseconds
const oldClosedRequests = await ctx.db
.query("requests")
.withIndex("by_closed", (q) => q.eq("closed", true))
.collect();
let deletedCount = 0;
for (const request of oldClosedRequests) {
if (request._creationTime < daysAgo && request.closedBy === "user") {
// remove from users array of requests
if (request.submittedBy) {
const user = await ctx.db.get(request.submittedBy);
if (user && user.requests) {
await ctx.db.patch(request.submittedBy, {
requests: user.requests.filter((id) => id !== request._id),
});
}
}
await ctx.db.delete(request._id);
deletedCount++;
}
}
return deletedCount;
},
});

View File

@@ -43,6 +43,7 @@ const requests = defineTable({
),
targetUrl: v.string(),
closed: v.boolean(),
closedBy: v.optional(v.union(v.literal("user"), v.literal("verification"))),
leaks: v.array(v.id("leaks")),
submittedBy: v.id("users"),
})

3
public/_headers Normal file
View File

@@ -0,0 +1,3 @@
/*
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'