diff --git a/.gitignore b/.gitignore index 3d7f46f..66758ca 100644 --- a/.gitignore +++ b/.gitignore @@ -86,7 +86,7 @@ jspm_packages/ # Gatsby files .cache/ -public + # Storybook build outputs .out diff --git a/convex/crons.ts b/convex/crons.ts new file mode 100644 index 0000000..1964000 --- /dev/null +++ b/convex/crons.ts @@ -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; \ No newline at end of file diff --git a/convex/leaks.ts b/convex/leaks.ts index 445e91e..55bcebf 100644 --- a/convex/leaks.ts +++ b/convex/leaks.ts @@ -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); diff --git a/convex/requests.ts b/convex/requests.ts index 326dbff..e314ddf 100644 --- a/convex/requests.ts +++ b/convex/requests.ts @@ -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; + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts index dda21dd..d119a62 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -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"), }) diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000..8aa4d44 --- /dev/null +++ b/public/_headers @@ -0,0 +1,3 @@ +/* + X-Frame-Options: DENY + Content-Security-Policy: frame-ancestors 'none' \ No newline at end of file