@techstream/quark-create-app 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techstream/quark-create-app",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "quark-create-app": "src/index.js",
package/src/index.js CHANGED
@@ -475,6 +475,24 @@ PORT=3000
475
475
 
476
476
  # --- Worker Configuration ---
477
477
  WORKER_CONCURRENCY=5
478
+
479
+ # --- File Storage Configuration ---
480
+ # Storage provider: "local" (default) or "s3" (S3-compatible, e.g. Cloudflare R2)
481
+ STORAGE_PROVIDER=local
482
+ # Local storage directory (only used when STORAGE_PROVIDER=local)
483
+ # STORAGE_LOCAL_DIR=./uploads
484
+
485
+ # S3 / Cloudflare R2 Configuration (only used when STORAGE_PROVIDER=s3)
486
+ # S3_BUCKET=your-bucket-name
487
+ # S3_REGION=auto
488
+ # S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
489
+ # S3_ACCESS_KEY_ID=your-access-key
490
+ # S3_SECRET_ACCESS_KEY=your-secret-key
491
+ # S3_PUBLIC_URL=https://your-public-bucket-domain.com
492
+
493
+ # --- Upload Limits ---
494
+ # UPLOAD_MAX_SIZE=10485760
495
+ # UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif,image/webp,image/avif,image/svg+xml,application/pdf
478
496
  `;
479
497
  await fs.writeFile(
480
498
  path.join(targetDir, ".env.example"),
@@ -540,6 +558,9 @@ PORT=${webPort}
540
558
 
541
559
  # --- Worker Configuration ---
542
560
  WORKER_CONCURRENCY=5
561
+
562
+ # --- File Storage ---
563
+ STORAGE_PROVIDER=local
543
564
  `;
544
565
  await fs.writeFile(path.join(targetDir, ".env"), envContent);
545
566
  console.log(
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Single File API
3
+ * GET /api/files/[id] - Download / serve a file
4
+ * DELETE /api/files/[id] - Delete a file (owner or admin only)
5
+ */
6
+
7
+ import {
8
+ createStorage,
9
+ ForbiddenError,
10
+ NotFoundError,
11
+ withCsrfProtection,
12
+ } from "@techstream/quark-core";
13
+ import { file } from "@techstream/quark-db";
14
+ import { NextResponse } from "next/server";
15
+ import { requireAuth } from "@/lib/auth-middleware";
16
+ import { handleError } from "../../error-handler";
17
+
18
+ /**
19
+ * GET /api/files/[id]
20
+ * Serve / download a file by its database ID.
21
+ * Public endpoint (no auth required) — access control is by knowledge of ID.
22
+ */
23
+ export async function GET(_request, { params }) {
24
+ try {
25
+ const { id } = await params;
26
+
27
+ const record = await file.findById(id);
28
+ if (!record) {
29
+ throw new NotFoundError("File not found");
30
+ }
31
+
32
+ const storage = createStorage();
33
+ const { body, contentType } = await storage.get(record.storageKey);
34
+
35
+ const headers = new Headers();
36
+ headers.set("Content-Type", contentType || record.mimeType);
37
+ headers.set("Content-Length", String(body.length));
38
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
39
+
40
+ // Inline display for images; attachment download for everything else
41
+ const isImage = record.mimeType.startsWith("image/");
42
+ const safeName = record.originalName.replace(/[\\"\r\n]/g, "_");
43
+ const encodedName = encodeURIComponent(record.originalName);
44
+ headers.set(
45
+ "Content-Disposition",
46
+ isImage
47
+ ? `inline; filename="${safeName}"; filename*=UTF-8''${encodedName}`
48
+ : `attachment; filename="${safeName}"; filename*=UTF-8''${encodedName}`,
49
+ );
50
+
51
+ return new Response(body, { status: 200, headers });
52
+ } catch (error) {
53
+ return handleError(error);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * DELETE /api/files/[id]
59
+ * Delete a file. Only the uploader or an admin can delete.
60
+ */
61
+ export const DELETE = withCsrfProtection(async (_request, { params }) => {
62
+ try {
63
+ const session = await requireAuth();
64
+ const { id } = await params;
65
+
66
+ const record = await file.findById(id);
67
+ if (!record) {
68
+ throw new NotFoundError("File not found");
69
+ }
70
+
71
+ // Authorization: uploader or admin
72
+ const isOwner = record.uploadedById === session.user.id;
73
+ const isAdmin = session.user.role === "admin";
74
+ if (!isOwner && !isAdmin) {
75
+ throw new ForbiddenError("You can only delete your own files");
76
+ }
77
+
78
+ // Remove from storage
79
+ const storage = createStorage();
80
+ await storage.delete(record.storageKey);
81
+
82
+ // Remove database record
83
+ await file.delete(record.id);
84
+
85
+ return NextResponse.json({ message: "File deleted" });
86
+ } catch (error) {
87
+ return handleError(error);
88
+ }
89
+ });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * File Upload API
3
+ * POST /api/files - Upload a file (multipart/form-data)
4
+ * GET /api/files - List current user's files
5
+ */
6
+
7
+ import {
8
+ createStorage,
9
+ generateStorageKey,
10
+ parseMultipart,
11
+ validateFile,
12
+ } from "@techstream/quark-core";
13
+ import { file } from "@techstream/quark-db";
14
+ import { NextResponse } from "next/server";
15
+ import { z } from "zod";
16
+ import { requireAuth } from "@/lib/auth-middleware";
17
+ import { handleError } from "../error-handler";
18
+
19
+ const paginationSchema = z.object({
20
+ page: z.coerce.number().int().min(1).default(1),
21
+ limit: z.coerce.number().int().min(1).max(100).default(50),
22
+ });
23
+
24
+ /**
25
+ * POST /api/files
26
+ * Upload one or more files via multipart/form-data.
27
+ * Requires authentication.
28
+ */
29
+ export async function POST(request) {
30
+ try {
31
+ const session = await requireAuth();
32
+
33
+ // Parse multipart body
34
+ const { files: parsedFiles } = await parseMultipart(request);
35
+
36
+ if (parsedFiles.length === 0) {
37
+ return NextResponse.json(
38
+ { message: "No files provided" },
39
+ { status: 400 },
40
+ );
41
+ }
42
+
43
+ const storage = createStorage();
44
+ const results = [];
45
+
46
+ for (const parsed of parsedFiles) {
47
+ // Validate each file
48
+ const validation = validateFile({
49
+ filename: parsed.filename,
50
+ mimeType: parsed.mimeType,
51
+ size: parsed.size,
52
+ buffer: parsed.buffer,
53
+ });
54
+
55
+ if (!validation.valid) {
56
+ return NextResponse.json(
57
+ { message: validation.error, filename: parsed.filename },
58
+ { status: 422 },
59
+ );
60
+ }
61
+
62
+ // Generate storage key and upload
63
+ const storageKey = generateStorageKey(parsed.filename);
64
+
65
+ await storage.put(storageKey, parsed.buffer, {
66
+ contentType: parsed.mimeType,
67
+ });
68
+
69
+ // Save metadata to database
70
+ const record = await file.create({
71
+ filename: storageKey.split("/").pop(),
72
+ originalName: parsed.filename,
73
+ mimeType: parsed.mimeType,
74
+ size: parsed.size,
75
+ storageKey,
76
+ storageProvider: storage.provider,
77
+ uploadedById: session.user.id,
78
+ });
79
+
80
+ results.push({
81
+ id: record.id,
82
+ originalName: record.originalName,
83
+ mimeType: record.mimeType,
84
+ size: record.size,
85
+ url: `/api/files/${record.id}`,
86
+ createdAt: record.createdAt,
87
+ });
88
+ }
89
+
90
+ const status = results.length === 1 ? 201 : 200;
91
+ const body = results.length === 1 ? results[0] : { files: results };
92
+
93
+ return NextResponse.json(body, { status });
94
+ } catch (error) {
95
+ return handleError(error);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * GET /api/files
101
+ * List files uploaded by the current user.
102
+ */
103
+ export async function GET(request) {
104
+ try {
105
+ const session = await requireAuth();
106
+
107
+ const { searchParams } = new URL(request.url);
108
+ const { page, limit } = paginationSchema.parse({
109
+ page: searchParams.get("page") ?? undefined,
110
+ limit: searchParams.get("limit") ?? undefined,
111
+ });
112
+
113
+ const skip = (page - 1) * limit;
114
+ const files = await file.findByUploader(session.user.id, {
115
+ skip,
116
+ take: limit,
117
+ });
118
+
119
+ const mapped = files.map((f) => ({
120
+ id: f.id,
121
+ originalName: f.originalName,
122
+ mimeType: f.mimeType,
123
+ size: f.size,
124
+ url: `/api/files/${f.id}`,
125
+ createdAt: f.createdAt,
126
+ }));
127
+
128
+ return NextResponse.json(mapped);
129
+ } catch (error) {
130
+ return handleError(error);
131
+ }
132
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Email Job Handlers
3
+ * Processes email-related background jobs
4
+ */
5
+
6
+ import {
7
+ createEmailService,
8
+ passwordResetEmail,
9
+ welcomeEmail,
10
+ } from "@techstream/quark-core";
11
+ import { prisma } from "@techstream/quark-db";
12
+ import { JOB_NAMES } from "@techstream/quark-jobs";
13
+
14
+ const emailService = createEmailService();
15
+
16
+ /**
17
+ * Job handler for SEND_WELCOME_EMAIL
18
+ * @param {import("bullmq").Job} bullJob
19
+ * @param {import("@techstream/quark-core").Logger} logger
20
+ */
21
+ export async function handleSendWelcomeEmail(bullJob, logger) {
22
+ const { userId } = bullJob.data;
23
+
24
+ if (!userId) {
25
+ throw new Error("userId is required for SEND_WELCOME_EMAIL job");
26
+ }
27
+
28
+ logger.info(`Sending welcome email for user ${userId}`, {
29
+ job: JOB_NAMES.SEND_WELCOME_EMAIL,
30
+ userId,
31
+ });
32
+
33
+ const userRecord = await prisma.user.findUnique({
34
+ where: { id: userId },
35
+ select: { email: true, name: true },
36
+ });
37
+
38
+ if (!userRecord?.email) {
39
+ throw new Error(`User ${userId} not found or has no email`);
40
+ }
41
+
42
+ const template = welcomeEmail({
43
+ name: userRecord.name,
44
+ loginUrl: process.env.APP_URL
45
+ ? `${process.env.APP_URL}/api/auth/signin`
46
+ : undefined,
47
+ });
48
+
49
+ await emailService.sendEmail(
50
+ userRecord.email,
51
+ template.subject,
52
+ template.html,
53
+ template.text,
54
+ );
55
+
56
+ return { success: true, userId, email: userRecord.email };
57
+ }
58
+
59
+ /**
60
+ * Job handler for SEND_RESET_PASSWORD_EMAIL
61
+ * @param {import("bullmq").Job} bullJob
62
+ * @param {import("@techstream/quark-core").Logger} logger
63
+ */
64
+ export async function handleSendResetPasswordEmail(bullJob, logger) {
65
+ const { userId, resetUrl } = bullJob.data;
66
+
67
+ if (!userId || !resetUrl) {
68
+ throw new Error(
69
+ "userId and resetUrl are required for SEND_RESET_PASSWORD_EMAIL job",
70
+ );
71
+ }
72
+
73
+ logger.info(`Sending password reset email for user ${userId}`, {
74
+ job: JOB_NAMES.SEND_RESET_PASSWORD_EMAIL,
75
+ userId,
76
+ });
77
+
78
+ const userRecord = await prisma.user.findUnique({
79
+ where: { id: userId },
80
+ select: { email: true, name: true },
81
+ });
82
+
83
+ if (!userRecord?.email) {
84
+ throw new Error(`User ${userId} not found or has no email`);
85
+ }
86
+
87
+ const template = passwordResetEmail({
88
+ name: userRecord.name,
89
+ resetUrl,
90
+ });
91
+
92
+ await emailService.sendEmail(
93
+ userRecord.email,
94
+ template.subject,
95
+ template.html,
96
+ template.text,
97
+ );
98
+
99
+ return { success: true, userId, email: userRecord.email };
100
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * File Job Handlers
3
+ * Processes file-related background jobs (cleanup, etc.)
4
+ */
5
+
6
+ import { createStorage } from "@techstream/quark-core";
7
+ import { file } from "@techstream/quark-db";
8
+ import { JOB_NAMES } from "@techstream/quark-jobs";
9
+
10
+ /**
11
+ * Job handler for CLEANUP_ORPHANED_FILES
12
+ * Deletes files with no owner that are older than a retention period.
13
+ *
14
+ * @param {import("bullmq").Job} bullJob
15
+ * @param {import("@techstream/quark-core").Logger} logger
16
+ */
17
+ export async function handleCleanupOrphanedFiles(bullJob, logger) {
18
+ const retentionHours = bullJob.data?.retentionHours || 24;
19
+ const cutoff = new Date(Date.now() - retentionHours * 60 * 60 * 1000);
20
+
21
+ logger.info("Starting orphaned file cleanup", {
22
+ job: JOB_NAMES.CLEANUP_ORPHANED_FILES,
23
+ retentionHours,
24
+ cutoff: cutoff.toISOString(),
25
+ });
26
+
27
+ const orphaned = await file.findOlderThan(cutoff);
28
+
29
+ if (orphaned.length === 0) {
30
+ logger.info("No orphaned files to clean up");
31
+ return { success: true, deleted: 0 };
32
+ }
33
+
34
+ const storage = createStorage();
35
+ let deleted = 0;
36
+ const errors = [];
37
+
38
+ for (const record of orphaned) {
39
+ try {
40
+ await storage.delete(record.storageKey);
41
+ await file.delete(record.id);
42
+ deleted++;
43
+ } catch (err) {
44
+ errors.push({ id: record.id, error: err.message });
45
+ logger.warn(`Failed to delete file ${record.id}: ${err.message}`);
46
+ }
47
+ }
48
+
49
+ logger.info(
50
+ `Orphaned file cleanup complete: ${deleted}/${orphaned.length} deleted`,
51
+ {
52
+ deleted,
53
+ total: orphaned.length,
54
+ errors: errors.length,
55
+ },
56
+ );
57
+
58
+ return { success: true, deleted, total: orphaned.length, errors };
59
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Job Handler Registry
3
+ * Maps job names to their handler functions.
4
+ * Each handler receives (bullJob, logger) and returns a result object.
5
+ */
6
+
7
+ import { JOB_NAMES } from "@techstream/quark-jobs";
8
+ import {
9
+ handleSendResetPasswordEmail,
10
+ handleSendWelcomeEmail,
11
+ } from "./email.js";
12
+ import { handleCleanupOrphanedFiles } from "./files.js";
13
+
14
+ export const jobHandlers = {
15
+ [JOB_NAMES.SEND_WELCOME_EMAIL]: handleSendWelcomeEmail,
16
+ [JOB_NAMES.SEND_RESET_PASSWORD_EMAIL]: handleSendResetPasswordEmail,
17
+ [JOB_NAMES.CLEANUP_ORPHANED_FILES]: handleCleanupOrphanedFiles,
18
+ };
@@ -5,163 +5,87 @@
5
5
  */
6
6
 
7
7
  import {
8
- createEmailService,
9
8
  createLogger,
9
+ createQueue,
10
10
  createWorker,
11
- passwordResetEmail,
12
- welcomeEmail,
13
11
  } from "@techstream/quark-core";
14
12
  import { prisma } from "@techstream/quark-db";
15
13
  import { JOB_NAMES, JOB_QUEUES } from "@techstream/quark-jobs";
14
+ import { jobHandlers } from "./handlers/index.js";
16
15
 
17
16
  const logger = createLogger("worker");
18
17
 
19
18
  // Store workers for graceful shutdown
20
19
  const workers = [];
21
20
 
22
- // Initialize email service
23
- const emailService = createEmailService();
24
-
25
21
  /**
26
- * Job handler for SEND_WELCOME_EMAIL
27
- * @param {Job} bullJob - BullMQ job object
22
+ * Generic queue processor — dispatches jobs to registered handlers
23
+ * @param {string} queueName
28
24
  */
29
- async function handleSendWelcomeEmail(bullJob) {
30
- const { userId } = bullJob.data;
31
-
32
- if (!userId) {
33
- throw new Error("userId is required for SEND_WELCOME_EMAIL job");
34
- }
35
-
36
- logger.info(`Sending welcome email for user ${userId}`, {
37
- job: JOB_NAMES.SEND_WELCOME_EMAIL,
38
- userId,
39
- });
40
-
41
- const userRecord = await prisma.user.findUnique({
42
- where: { id: userId },
43
- select: { email: true, name: true },
44
- });
45
-
46
- if (!userRecord?.email) {
47
- throw new Error(`User ${userId} not found or has no email`);
48
- }
49
-
50
- const template = welcomeEmail({
51
- name: userRecord.name,
52
- loginUrl: process.env.APP_URL
53
- ? `${process.env.APP_URL}/api/auth/signin`
54
- : undefined,
55
- });
56
-
57
- await emailService.sendEmail(
58
- userRecord.email,
59
- template.subject,
60
- template.html,
61
- template.text,
25
+ function createQueueWorker(queueName) {
26
+ const queueWorker = createWorker(
27
+ queueName,
28
+ async (bullJob) => {
29
+ const handler = jobHandlers[bullJob.name];
30
+
31
+ if (!handler) {
32
+ throw new Error(`No handler registered for job: ${bullJob.name}`);
33
+ }
34
+
35
+ return handler(bullJob, logger);
36
+ },
37
+ {
38
+ concurrency: parseInt(process.env.WORKER_CONCURRENCY || "5", 10),
39
+ },
62
40
  );
63
41
 
64
- return { success: true, userId, email: userRecord.email };
65
- }
66
-
67
- /**
68
- * Job handler for SEND_RESET_PASSWORD_EMAIL
69
- * @param {Job} bullJob - BullMQ job object
70
- */
71
- async function handleSendResetPasswordEmail(bullJob) {
72
- const { userId, resetUrl } = bullJob.data;
42
+ workers.push(queueWorker);
73
43
 
74
- if (!userId || !resetUrl) {
75
- throw new Error(
76
- "userId and resetUrl are required for SEND_RESET_PASSWORD_EMAIL job",
77
- );
78
- }
79
-
80
- logger.info(`Sending password reset email for user ${userId}`, {
81
- job: JOB_NAMES.SEND_RESET_PASSWORD_EMAIL,
82
- userId,
44
+ queueWorker.on("completed", (job, result) => {
45
+ logger.info(`Job ${job.id} (${job.name}) completed`, { result });
83
46
  });
84
47
 
85
- const userRecord = await prisma.user.findUnique({
86
- where: { id: userId },
87
- select: { email: true, name: true },
88
- });
89
-
90
- if (!userRecord?.email) {
91
- throw new Error(`User ${userId} not found or has no email`);
92
- }
93
-
94
- const template = passwordResetEmail({
95
- name: userRecord.name,
96
- resetUrl,
48
+ queueWorker.on("failed", (job, error) => {
49
+ logger.error(
50
+ `Job ${job.id} (${job.name}) failed after ${job.attemptsMade} attempts`,
51
+ {
52
+ error: error.message,
53
+ jobName: job.name,
54
+ attemptsMade: job.attemptsMade,
55
+ },
56
+ );
97
57
  });
98
58
 
99
- await emailService.sendEmail(
100
- userRecord.email,
101
- template.subject,
102
- template.html,
103
- template.text,
59
+ logger.info(
60
+ `Queue "${queueName}" worker started (concurrency: ${queueWorker.opts.concurrency})`,
104
61
  );
105
62
 
106
- return { success: true, userId, email: userRecord.email };
63
+ return queueWorker;
107
64
  }
108
65
 
109
- /**
110
- * Initialize job handlers
111
- * Maps job names to their handler functions
112
- */
113
- const jobHandlers = {
114
- [JOB_NAMES.SEND_WELCOME_EMAIL]: handleSendWelcomeEmail,
115
- [JOB_NAMES.SEND_RESET_PASSWORD_EMAIL]: handleSendResetPasswordEmail,
116
- };
117
-
118
66
  /**
119
67
  * Start the worker service
120
- * Creates workers for all queues and registers handlers
121
68
  */
122
69
  async function startWorker() {
123
70
  logger.info("Starting Quark Worker Service");
124
71
 
125
72
  try {
126
- // Create worker for email queue
127
- const emailQueueWorker = createWorker(
128
- JOB_QUEUES.EMAIL,
129
- async (bullJob) => {
130
- const handler = jobHandlers[bullJob.name];
131
-
132
- if (!handler) {
133
- throw new Error(`No handler registered for job: ${bullJob.name}`);
134
- }
73
+ // Register a worker for each queue
74
+ for (const queueName of Object.values(JOB_QUEUES)) {
75
+ createQueueWorker(queueName);
76
+ }
135
77
 
136
- return handler(bullJob);
137
- },
78
+ // Schedule repeating cleanup job (runs every 24 hours)
79
+ const filesQueue = createQueue(JOB_QUEUES.FILES);
80
+ await filesQueue.add(
81
+ JOB_NAMES.CLEANUP_ORPHANED_FILES,
82
+ { retentionHours: 24 },
138
83
  {
139
- concurrency: parseInt(process.env.WORKER_CONCURRENCY || "5", 10),
84
+ repeat: { every: 24 * 60 * 60 * 1000 }, // 24h
85
+ jobId: "cleanup-orphaned-files-repeat",
140
86
  },
141
87
  );
142
88
 
143
- workers.push(emailQueueWorker);
144
-
145
- emailQueueWorker.on("completed", (job, result) => {
146
- logger.info(`Job ${job.id} (${job.name}) completed`, { result });
147
- });
148
-
149
- emailQueueWorker.on("failed", (job, error) => {
150
- logger.error(
151
- `Job ${job.id} (${job.name}) failed after ${job.attemptsMade} attempts`,
152
- {
153
- error: error.message,
154
- jobName: job.name,
155
- attemptsMade: job.attemptsMade,
156
- },
157
- );
158
- });
159
-
160
- logger.info(
161
- `Email queue worker started (concurrency: ${emailQueueWorker.opts.concurrency})`,
162
- );
163
-
164
- // Ready to process jobs
165
89
  logger.info("Worker service ready");
166
90
  } catch (error) {
167
91
  logger.error("Failed to start worker service", {
@@ -179,12 +103,10 @@ async function shutdown() {
179
103
  logger.info("Shutting down worker service");
180
104
 
181
105
  try {
182
- // Close all workers
183
106
  for (const worker of workers) {
184
107
  await worker.close();
185
108
  }
186
109
 
187
- // Disconnect Prisma client
188
110
  await prisma.$disconnect();
189
111
 
190
112
  logger.info("All workers closed");
@@ -198,6 +120,11 @@ async function shutdown() {
198
120
  }
199
121
  }
200
122
 
123
+ process.on("SIGTERM", shutdown);
124
+ process.on("SIGINT", shutdown);
125
+
126
+ startWorker();
127
+
201
128
  // Register shutdown handlers
202
129
  process.on("SIGTERM", shutdown);
203
130
  process.on("SIGINT", shutdown);
@@ -34,7 +34,17 @@
34
34
  "MAILHOG_UI_PORT",
35
35
  "NEXTAUTH_SECRET",
36
36
  "APP_URL",
37
- "WORKER_CONCURRENCY"
37
+ "WORKER_CONCURRENCY",
38
+ "STORAGE_PROVIDER",
39
+ "STORAGE_LOCAL_DIR",
40
+ "S3_BUCKET",
41
+ "S3_REGION",
42
+ "S3_ENDPOINT",
43
+ "S3_ACCESS_KEY_ID",
44
+ "S3_SECRET_ACCESS_KEY",
45
+ "S3_PUBLIC_URL",
46
+ "UPLOAD_MAX_SIZE",
47
+ "UPLOAD_ALLOWED_TYPES"
38
48
  ]
39
49
  }
40
50
  }
@@ -1,9 +1,10 @@
1
1
  export const JOB_QUEUES = {
2
2
  EMAIL: "email-queue",
3
- NOTIFICATIONS: "notifications-queue",
3
+ FILES: "files-queue",
4
4
  };
5
5
 
6
6
  export const JOB_NAMES = {
7
7
  SEND_WELCOME_EMAIL: "send-welcome-email",
8
8
  SEND_RESET_PASSWORD_EMAIL: "send-reset-password-email",
9
+ CLEANUP_ORPHANED_FILES: "cleanup-orphaned-files",
9
10
  };
@@ -1,2 +1 @@
1
1
  export { JOB_NAMES, JOB_QUEUES } from "./definitions.js";
2
- export { jobHandlers } from "./handlers.js";
@@ -1,20 +0,0 @@
1
- import { JOB_NAMES } from "./definitions.js";
2
-
3
- export async function sendWelcomeEmail(job) {
4
- const { email } = job.data;
5
- console.log(`Sending welcome email to ${email}`);
6
- // Add your email sending logic here
7
- return { success: true };
8
- }
9
-
10
- export async function sendResetPasswordEmail(job) {
11
- const { email } = job.data;
12
- console.log(`Sending reset password email to ${email}`);
13
- // Add your email sending logic here
14
- return { success: true };
15
- }
16
-
17
- export const jobHandlers = {
18
- [JOB_NAMES.SEND_WELCOME_EMAIL]: sendWelcomeEmail,
19
- [JOB_NAMES.SEND_RESET_PASSWORD_EMAIL]: sendResetPasswordEmail,
20
- };