@techstream/quark-create-app 1.2.0 → 1.4.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 +34 -33
- package/src/index.js +193 -56
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +17 -1
- package/templates/base-project/apps/web/src/app/api/error-handler.js +4 -2
- package/templates/base-project/apps/web/src/app/api/health/route.js +8 -3
- package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +9 -5
- package/templates/base-project/apps/web/src/app/api/posts/route.js +13 -5
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +9 -9
- package/templates/base-project/apps/web/src/app/api/users/route.js +6 -6
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +18 -1
- package/templates/base-project/apps/web/src/{middleware.js → proxy.js} +3 -3
- package/templates/base-project/apps/worker/package.json +1 -2
- package/templates/base-project/apps/worker/src/index.js +71 -19
- package/templates/base-project/docker-compose.yml +3 -6
- package/templates/base-project/package.json +16 -1
- package/templates/base-project/packages/db/package.json +10 -4
- package/templates/base-project/packages/db/prisma.config.ts +2 -2
- package/templates/base-project/packages/db/scripts/seed.js +1 -1
- package/templates/base-project/packages/db/src/client.js +41 -25
- package/templates/base-project/packages/db/src/queries.js +22 -9
- package/templates/base-project/packages/db/src/schemas.js +6 -1
- package/templates/base-project/turbo.json +17 -1
- package/templates/config/package.json +3 -1
- package/templates/config/src/app-url.js +71 -0
- package/templates/config/src/validate-env.js +104 -0
- package/templates/base-project/packages/db/src/generated/prisma/browser.ts +0 -53
- package/templates/base-project/packages/db/src/generated/prisma/client.ts +0 -82
- package/templates/base-project/packages/db/src/generated/prisma/commonInputTypes.ts +0 -649
- package/templates/base-project/packages/db/src/generated/prisma/enums.ts +0 -19
- package/templates/base-project/packages/db/src/generated/prisma/internal/class.ts +0 -305
- package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespace.ts +0 -1428
- package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespaceBrowser.ts +0 -217
- package/templates/base-project/packages/db/src/generated/prisma/models/Account.ts +0 -2098
- package/templates/base-project/packages/db/src/generated/prisma/models/AuditLog.ts +0 -1805
- package/templates/base-project/packages/db/src/generated/prisma/models/Job.ts +0 -1737
- package/templates/base-project/packages/db/src/generated/prisma/models/Post.ts +0 -1762
- package/templates/base-project/packages/db/src/generated/prisma/models/Session.ts +0 -1738
- package/templates/base-project/packages/db/src/generated/prisma/models/User.ts +0 -2298
- package/templates/base-project/packages/db/src/generated/prisma/models/VerificationToken.ts +0 -1450
- package/templates/base-project/packages/db/src/generated/prisma/models.ts +0 -18
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { validateBody } from "@techstream/quark-core";
|
|
1
|
+
import { validateBody, withCsrfProtection } from "@techstream/quark-core";
|
|
2
2
|
import { user, userCreateSchema } from "@techstream/quark-db";
|
|
3
3
|
import { NextResponse } from "next/server";
|
|
4
|
-
import {
|
|
4
|
+
import { requireRole } from "@/lib/auth-middleware";
|
|
5
5
|
import { handleError } from "../error-handler";
|
|
6
6
|
|
|
7
7
|
export async function GET(_request) {
|
|
8
8
|
try {
|
|
9
|
-
await
|
|
9
|
+
await requireRole("admin");
|
|
10
10
|
const users = await user.findAll();
|
|
11
11
|
return NextResponse.json(users);
|
|
12
12
|
} catch (error) {
|
|
@@ -14,9 +14,9 @@ export async function GET(_request) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export const POST = withCsrfProtection(async (request) => {
|
|
18
18
|
try {
|
|
19
|
-
await
|
|
19
|
+
await requireRole("admin");
|
|
20
20
|
const data = await validateBody(request, userCreateSchema);
|
|
21
21
|
|
|
22
22
|
// Check if email already exists
|
|
@@ -33,4 +33,4 @@ export async function POST(request) {
|
|
|
33
33
|
} catch (error) {
|
|
34
34
|
return handleError(error);
|
|
35
35
|
}
|
|
36
|
-
}
|
|
36
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UnauthorizedError } from "@techstream/quark-core";
|
|
1
|
+
import { ForbiddenError, UnauthorizedError } from "@techstream/quark-core";
|
|
2
2
|
import { auth } from "./auth";
|
|
3
3
|
|
|
4
4
|
export async function requireAuth() {
|
|
@@ -12,3 +12,20 @@ export async function requireAuth() {
|
|
|
12
12
|
|
|
13
13
|
return session;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Require the current user to have a specific role.
|
|
18
|
+
* @param {string} role - Required role (e.g. "admin")
|
|
19
|
+
* @returns {Promise<import("next-auth").Session>}
|
|
20
|
+
*/
|
|
21
|
+
export async function requireRole(role) {
|
|
22
|
+
const session = await requireAuth();
|
|
23
|
+
|
|
24
|
+
if (session.user?.role !== role) {
|
|
25
|
+
throw new ForbiddenError(
|
|
26
|
+
"You do not have permission to access this resource",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return session;
|
|
31
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Next.js
|
|
2
|
+
* Next.js Proxy
|
|
3
3
|
* Handles rate limiting, CORS, and security headers
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -112,7 +112,7 @@ const REQUEST_SIZE_LIMITS = {
|
|
|
112
112
|
upload: parseInt(process.env.UPLOAD_SIZE_LIMIT || "10485760", 10), // 10MB for uploads
|
|
113
113
|
};
|
|
114
114
|
|
|
115
|
-
export function
|
|
115
|
+
export function proxy(request) {
|
|
116
116
|
const { pathname } = request.nextUrl;
|
|
117
117
|
const origin = request.headers.get("origin") || "";
|
|
118
118
|
|
|
@@ -250,7 +250,7 @@ export function middleware(request) {
|
|
|
250
250
|
return response;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
// Configure which routes the
|
|
253
|
+
// Configure which routes the proxy runs on
|
|
254
254
|
export const config = {
|
|
255
255
|
matcher: [
|
|
256
256
|
/*
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
"@techstream/quark-core": "^1.0.0",
|
|
18
18
|
"@techstream/quark-db": "workspace:*",
|
|
19
19
|
"@techstream/quark-jobs": "workspace:*",
|
|
20
|
-
"bullmq": "^5.64.1"
|
|
21
|
-
"dotenv": "^17.2.3"
|
|
20
|
+
"bullmq": "^5.64.1"
|
|
22
21
|
},
|
|
23
22
|
"devDependencies": {
|
|
24
23
|
"@techstream/quark-config": "workspace:*",
|
|
@@ -8,13 +8,11 @@ import {
|
|
|
8
8
|
createEmailService,
|
|
9
9
|
createLogger,
|
|
10
10
|
createWorker,
|
|
11
|
+
passwordResetEmail,
|
|
12
|
+
welcomeEmail,
|
|
11
13
|
} from "@techstream/quark-core";
|
|
12
|
-
import { JOB_NAMES, JOB_QUEUES } from "@techstream/quark-jobs";
|
|
13
14
|
import { prisma } from "@techstream/quark-db";
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
// Load environment variables
|
|
17
|
-
dotenv.config();
|
|
15
|
+
import { JOB_NAMES, JOB_QUEUES } from "@techstream/quark-jobs";
|
|
18
16
|
|
|
19
17
|
const logger = createLogger("worker");
|
|
20
18
|
|
|
@@ -40,7 +38,6 @@ async function handleSendWelcomeEmail(bullJob) {
|
|
|
40
38
|
userId,
|
|
41
39
|
});
|
|
42
40
|
|
|
43
|
-
// Look up the user's email
|
|
44
41
|
const userRecord = await prisma.user.findUnique({
|
|
45
42
|
where: { id: userId },
|
|
46
43
|
select: { email: true, name: true },
|
|
@@ -50,15 +47,60 @@ async function handleSendWelcomeEmail(bullJob) {
|
|
|
50
47
|
throw new Error(`User ${userId} not found or has no email`);
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
const
|
|
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,
|
|
62
|
+
);
|
|
63
|
+
|
|
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;
|
|
73
|
+
|
|
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,
|
|
83
|
+
});
|
|
84
|
+
|
|
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,
|
|
97
|
+
});
|
|
54
98
|
|
|
55
99
|
await emailService.sendEmail(
|
|
56
100
|
userRecord.email,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<p>You can now sign in and start using the application.</p>`,
|
|
61
|
-
`Welcome, ${displayName}!\n\nYour account has been created successfully.\nYou can now sign in and start using the application.`,
|
|
101
|
+
template.subject,
|
|
102
|
+
template.html,
|
|
103
|
+
template.text,
|
|
62
104
|
);
|
|
63
105
|
|
|
64
106
|
return { success: true, userId, email: userRecord.email };
|
|
@@ -70,6 +112,7 @@ async function handleSendWelcomeEmail(bullJob) {
|
|
|
70
112
|
*/
|
|
71
113
|
const jobHandlers = {
|
|
72
114
|
[JOB_NAMES.SEND_WELCOME_EMAIL]: handleSendWelcomeEmail,
|
|
115
|
+
[JOB_NAMES.SEND_RESET_PASSWORD_EMAIL]: handleSendResetPasswordEmail,
|
|
73
116
|
};
|
|
74
117
|
|
|
75
118
|
/**
|
|
@@ -104,11 +147,14 @@ async function startWorker() {
|
|
|
104
147
|
});
|
|
105
148
|
|
|
106
149
|
emailQueueWorker.on("failed", (job, error) => {
|
|
107
|
-
logger.error(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
);
|
|
112
158
|
});
|
|
113
159
|
|
|
114
160
|
logger.info(
|
|
@@ -118,7 +164,10 @@ async function startWorker() {
|
|
|
118
164
|
// Ready to process jobs
|
|
119
165
|
logger.info("Worker service ready");
|
|
120
166
|
} catch (error) {
|
|
121
|
-
logger.error("Failed to start worker service", {
|
|
167
|
+
logger.error("Failed to start worker service", {
|
|
168
|
+
error: error.message,
|
|
169
|
+
stack: error.stack,
|
|
170
|
+
});
|
|
122
171
|
process.exit(1);
|
|
123
172
|
}
|
|
124
173
|
}
|
|
@@ -141,7 +190,10 @@ async function shutdown() {
|
|
|
141
190
|
logger.info("All workers closed");
|
|
142
191
|
process.exit(0);
|
|
143
192
|
} catch (error) {
|
|
144
|
-
logger.error("Error during shutdown", {
|
|
193
|
+
logger.error("Error during shutdown", {
|
|
194
|
+
error: error.message,
|
|
195
|
+
stack: error.stack,
|
|
196
|
+
});
|
|
145
197
|
process.exit(1);
|
|
146
198
|
}
|
|
147
199
|
}
|
|
@@ -2,7 +2,6 @@ services:
|
|
|
2
2
|
# --- 1. PostgreSQL Database ---
|
|
3
3
|
postgres:
|
|
4
4
|
image: postgres:16-alpine
|
|
5
|
-
container_name: postgres
|
|
6
5
|
restart: always
|
|
7
6
|
ports:
|
|
8
7
|
- "${POSTGRES_PORT:-5432}:5432"
|
|
@@ -16,7 +15,6 @@ services:
|
|
|
16
15
|
# --- 2. Redis Cache & Job Queue ---
|
|
17
16
|
redis:
|
|
18
17
|
image: redis:7-alpine
|
|
19
|
-
container_name: redis
|
|
20
18
|
restart: always
|
|
21
19
|
ports:
|
|
22
20
|
- "${REDIS_PORT:-6379}:6379"
|
|
@@ -24,10 +22,9 @@ services:
|
|
|
24
22
|
volumes:
|
|
25
23
|
- redis_data:/data
|
|
26
24
|
|
|
27
|
-
# --- 3.
|
|
28
|
-
|
|
29
|
-
image:
|
|
30
|
-
container_name: mailhog
|
|
25
|
+
# --- 3. Mailpit (Local SMTP Server) ---
|
|
26
|
+
mailpit:
|
|
27
|
+
image: ghcr.io/axllent/mailpit
|
|
31
28
|
restart: always
|
|
32
29
|
ports:
|
|
33
30
|
# SMTP port (used by application to send mail)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "turbo run build",
|
|
9
|
-
"dev": "turbo run dev",
|
|
9
|
+
"dev": "dotenv -- turbo run dev",
|
|
10
10
|
"lint": "turbo run lint",
|
|
11
11
|
"test": "turbo run test",
|
|
12
12
|
"docker:up": "docker compose up -d",
|
|
@@ -17,9 +17,24 @@
|
|
|
17
17
|
"author": "",
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"packageManager": "pnpm@10.12.1",
|
|
20
|
+
"pnpm": {
|
|
21
|
+
"onlyBuiltDependencies": [
|
|
22
|
+
"@prisma/engines",
|
|
23
|
+
"esbuild",
|
|
24
|
+
"msgpackr-extract",
|
|
25
|
+
"prisma",
|
|
26
|
+
"sharp"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencyRules": {
|
|
29
|
+
"allowedVersions": {
|
|
30
|
+
"nodemailer": "*"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
20
34
|
"devDependencies": {
|
|
21
35
|
"@biomejs/biome": "^2.3.13",
|
|
22
36
|
"@types/node": "^24.10.9",
|
|
37
|
+
"dotenv-cli": "^11.0.0",
|
|
23
38
|
"tsx": "^4.21.0",
|
|
24
39
|
"turbo": "^2.8.1"
|
|
25
40
|
}
|
|
@@ -10,20 +10,26 @@
|
|
|
10
10
|
"db:generate": "prisma generate",
|
|
11
11
|
"db:migrate": "prisma migrate dev",
|
|
12
12
|
"db:push": "prisma db push",
|
|
13
|
-
"db:seed": "
|
|
13
|
+
"db:seed": "prisma db seed",
|
|
14
14
|
"db:studio": "prisma studio"
|
|
15
15
|
},
|
|
16
|
+
"prisma": {
|
|
17
|
+
"seed": "node scripts/seed.js"
|
|
18
|
+
},
|
|
16
19
|
"keywords": [],
|
|
17
20
|
"author": "",
|
|
18
21
|
"license": "ISC",
|
|
19
22
|
"packageManager": "pnpm@10.12.1",
|
|
20
23
|
"devDependencies": {
|
|
21
24
|
"@techstream/quark-config": "workspace:*",
|
|
22
|
-
"
|
|
25
|
+
"bcryptjs": "^3.0.3",
|
|
26
|
+
"prisma": "^7.3.0"
|
|
23
27
|
},
|
|
24
28
|
"dependencies": {
|
|
25
|
-
"@prisma/
|
|
26
|
-
"
|
|
29
|
+
"@prisma/adapter-pg": "^7.3.0",
|
|
30
|
+
"@prisma/client": "^7.3.0",
|
|
31
|
+
"dotenv": "^17.2.4",
|
|
32
|
+
"pg": "^8.18.0",
|
|
27
33
|
"zod": "^4.3.6"
|
|
28
34
|
}
|
|
29
35
|
}
|
|
@@ -2,8 +2,8 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { config } from "dotenv";
|
|
3
3
|
import { defineConfig } from "prisma/config";
|
|
4
4
|
|
|
5
|
-
// Load .env from monorepo root
|
|
6
|
-
config({ path: resolve(__dirname, "../../.env") });
|
|
5
|
+
// Load .env from monorepo root (needed for standalone commands like db:push, db:seed)
|
|
6
|
+
config({ path: resolve(__dirname, "../../.env"), quiet: true });
|
|
7
7
|
|
|
8
8
|
// Construct DATABASE_URL from individual env vars - single source of truth
|
|
9
9
|
const user = process.env.POSTGRES_USER || "quark_user";
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
2
2
|
import { PrismaClient } from "./generated/prisma/client.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
if (!
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
4
|
+
/**
|
|
5
|
+
* Builds a Postgres connection string from individual env vars (mirrors prisma.config.ts).
|
|
6
|
+
* Throws if any required variable is missing — but only when actually called,
|
|
7
|
+
* so the module can be safely imported at build time (e.g. during `next build`).
|
|
8
|
+
*/
|
|
9
|
+
function getConnectionString() {
|
|
10
|
+
const user = process.env.POSTGRES_USER;
|
|
11
|
+
if (!user) throw new Error("POSTGRES_USER environment variable is required");
|
|
12
|
+
const password = process.env.POSTGRES_PASSWORD;
|
|
13
|
+
if (!password)
|
|
14
|
+
throw new Error("POSTGRES_PASSWORD environment variable is required");
|
|
15
|
+
const host = process.env.POSTGRES_HOST || "localhost";
|
|
16
|
+
const port = process.env.POSTGRES_PORT || "5432";
|
|
17
|
+
const db = process.env.POSTGRES_DB;
|
|
18
|
+
if (!db) throw new Error("POSTGRES_DB environment variable is required");
|
|
19
|
+
return `postgresql://${user}:${password}@${host}:${port}/${db}?schema=public`;
|
|
20
|
+
}
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* Returns the connection pool configuration for the `pg` driver.
|
|
@@ -26,6 +30,7 @@ const isProduction = process.env.NODE_ENV === "production";
|
|
|
26
30
|
* @returns {{ max: number, idleTimeoutMillis: number, connectionTimeoutMillis: number }}
|
|
27
31
|
*/
|
|
28
32
|
export function getPoolConfig() {
|
|
33
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
29
34
|
return {
|
|
30
35
|
max: Number(process.env.DB_POOL_MAX) || (isProduction ? 10 : 5),
|
|
31
36
|
idleTimeoutMillis: Number(process.env.DB_POOL_IDLE_TIMEOUT) || 30_000,
|
|
@@ -34,19 +39,30 @@ export function getPoolConfig() {
|
|
|
34
39
|
};
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
//
|
|
42
|
+
// Lazy singleton — the client is created on first property access, not at import time.
|
|
43
|
+
// This allows Next.js to import the module at build time without requiring DB env vars.
|
|
38
44
|
const globalForPrisma = globalThis;
|
|
39
|
-
export const prisma =
|
|
40
|
-
globalForPrisma.prisma ||
|
|
41
|
-
new PrismaClient({
|
|
42
|
-
adapter: new PrismaPg({
|
|
43
|
-
connectionString,
|
|
44
|
-
...getPoolConfig(),
|
|
45
|
-
}),
|
|
46
|
-
});
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
globalForPrisma.
|
|
46
|
+
function getPrismaClient() {
|
|
47
|
+
if (!globalForPrisma.__prisma) {
|
|
48
|
+
globalForPrisma.__prisma = new PrismaClient({
|
|
49
|
+
adapter: new PrismaPg({
|
|
50
|
+
connectionString: getConnectionString(),
|
|
51
|
+
...getPoolConfig(),
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return globalForPrisma.__prisma;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Prisma client singleton. Lazily initialized on first use so the module
|
|
60
|
+
* can be imported safely at build time (no DB env vars needed).
|
|
61
|
+
*/
|
|
62
|
+
export const prisma = new Proxy(/** @type {PrismaClient} */ ({}), {
|
|
63
|
+
get(_target, prop) {
|
|
64
|
+
return getPrismaClient()[prop];
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export * from "./generated/prisma/client.ts";
|
|
@@ -67,12 +67,17 @@ export const user = {
|
|
|
67
67
|
},
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Safe author include — returns author without sensitive fields.
|
|
72
|
+
*/
|
|
73
|
+
const AUTHOR_SAFE_INCLUDE = { author: { select: USER_SAFE_SELECT } };
|
|
74
|
+
|
|
70
75
|
// Post queries
|
|
71
76
|
export const post = {
|
|
72
77
|
findById: (id) => {
|
|
73
78
|
return prisma.post.findUnique({
|
|
74
79
|
where: { id },
|
|
75
|
-
include:
|
|
80
|
+
include: AUTHOR_SAFE_INCLUDE,
|
|
76
81
|
});
|
|
77
82
|
},
|
|
78
83
|
findAll: (options = {}) => {
|
|
@@ -80,7 +85,7 @@ export const post = {
|
|
|
80
85
|
return prisma.post.findMany({
|
|
81
86
|
skip,
|
|
82
87
|
take,
|
|
83
|
-
include:
|
|
88
|
+
include: AUTHOR_SAFE_INCLUDE,
|
|
84
89
|
orderBy: { createdAt: "desc" },
|
|
85
90
|
});
|
|
86
91
|
},
|
|
@@ -90,7 +95,7 @@ export const post = {
|
|
|
90
95
|
where: { published: true },
|
|
91
96
|
skip,
|
|
92
97
|
take,
|
|
93
|
-
include:
|
|
98
|
+
include: AUTHOR_SAFE_INCLUDE,
|
|
94
99
|
orderBy: { createdAt: "desc" },
|
|
95
100
|
});
|
|
96
101
|
},
|
|
@@ -100,21 +105,21 @@ export const post = {
|
|
|
100
105
|
where: { authorId },
|
|
101
106
|
skip,
|
|
102
107
|
take,
|
|
103
|
-
include:
|
|
108
|
+
include: AUTHOR_SAFE_INCLUDE,
|
|
104
109
|
orderBy: { createdAt: "desc" },
|
|
105
110
|
});
|
|
106
111
|
},
|
|
107
112
|
create: (data) => {
|
|
108
113
|
return prisma.post.create({
|
|
109
114
|
data,
|
|
110
|
-
include:
|
|
115
|
+
include: AUTHOR_SAFE_INCLUDE,
|
|
111
116
|
});
|
|
112
117
|
},
|
|
113
118
|
update: (id, data) => {
|
|
114
119
|
return prisma.post.update({
|
|
115
120
|
where: { id },
|
|
116
121
|
data,
|
|
117
|
-
include:
|
|
122
|
+
include: AUTHOR_SAFE_INCLUDE,
|
|
118
123
|
});
|
|
119
124
|
},
|
|
120
125
|
delete: (id) => {
|
|
@@ -122,7 +127,15 @@ export const post = {
|
|
|
122
127
|
where: { id },
|
|
123
128
|
});
|
|
124
129
|
},
|
|
125
|
-
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Note: Job tracking is handled by BullMQ's built-in Redis persistence.
|
|
133
|
+
// The Prisma Job model is retained in the schema for optional audit/reporting
|
|
134
|
+
// but these query helpers have been removed to avoid confusion with BullMQ.
|
|
135
|
+
// If you need database-backed job auditing, re-add job queries here and wire
|
|
136
|
+
// the worker to write status updates to the Job table.
|
|
137
|
+
|
|
138
|
+
// Account queries (NextAuth)
|
|
126
139
|
export const account = {
|
|
127
140
|
findById: (id) => {
|
|
128
141
|
return prisma.account.findUnique({
|
|
@@ -156,7 +169,7 @@ export const session = {
|
|
|
156
169
|
findByToken: (sessionToken) => {
|
|
157
170
|
return prisma.session.findUnique({
|
|
158
171
|
where: { sessionToken },
|
|
159
|
-
include: { user:
|
|
172
|
+
include: { user: { select: USER_SAFE_SELECT } },
|
|
160
173
|
});
|
|
161
174
|
},
|
|
162
175
|
findByUserId: (userId) => {
|
|
@@ -167,7 +180,7 @@ export const session = {
|
|
|
167
180
|
create: (data) => {
|
|
168
181
|
return prisma.session.create({
|
|
169
182
|
data,
|
|
170
|
-
include: { user:
|
|
183
|
+
include: { user: { select: USER_SAFE_SELECT } },
|
|
171
184
|
});
|
|
172
185
|
},
|
|
173
186
|
update: (sessionToken, data) => {
|
|
@@ -8,7 +8,12 @@ export const userCreateSchema = z.object({
|
|
|
8
8
|
|
|
9
9
|
export const userRegisterSchema = z.object({
|
|
10
10
|
email: z.string().email("Invalid email address"),
|
|
11
|
-
password: z
|
|
11
|
+
password: z
|
|
12
|
+
.string()
|
|
13
|
+
.min(8, "Password must be at least 8 characters")
|
|
14
|
+
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
|
|
15
|
+
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
|
|
16
|
+
.regex(/[0-9]/, "Password must contain at least one number"),
|
|
12
17
|
name: z.string().min(2, "Name must be at least 2 characters").optional(),
|
|
13
18
|
});
|
|
14
19
|
|
|
@@ -19,7 +19,23 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dev": {
|
|
21
21
|
"cache": false,
|
|
22
|
-
"persistent": true
|
|
22
|
+
"persistent": true,
|
|
23
|
+
"passThroughEnv": [
|
|
24
|
+
"PORT",
|
|
25
|
+
"POSTGRES_HOST",
|
|
26
|
+
"POSTGRES_PORT",
|
|
27
|
+
"POSTGRES_USER",
|
|
28
|
+
"POSTGRES_PASSWORD",
|
|
29
|
+
"POSTGRES_DB",
|
|
30
|
+
"REDIS_HOST",
|
|
31
|
+
"REDIS_PORT",
|
|
32
|
+
"MAILHOG_HOST",
|
|
33
|
+
"MAILHOG_SMTP_PORT",
|
|
34
|
+
"MAILHOG_UI_PORT",
|
|
35
|
+
"NEXTAUTH_SECRET",
|
|
36
|
+
"APP_URL",
|
|
37
|
+
"WORKER_CONCURRENCY"
|
|
38
|
+
]
|
|
23
39
|
}
|
|
24
40
|
}
|
|
25
41
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APP_URL — Single source of truth for the application's canonical URL.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. APP_URL (production — set to your real domain)
|
|
6
|
+
* 2. NEXTAUTH_URL (legacy / backward-compat)
|
|
7
|
+
* 3. http://localhost:${PORT || 3000} (local dev fallback)
|
|
8
|
+
*
|
|
9
|
+
* In development PORT is the single source of truth for the web server port.
|
|
10
|
+
* APP_URL is derived from it automatically so the two can never drift.
|
|
11
|
+
* In production, set APP_URL explicitly (e.g. https://yourdomain.com).
|
|
12
|
+
*
|
|
13
|
+
* Derived values:
|
|
14
|
+
* - NEXTAUTH_URL — always set equal to the resolved APP_URL so
|
|
15
|
+
* NextAuth works without a separate variable.
|
|
16
|
+
* - allowedOrigins — the resolved APP_URL plus any extra origins
|
|
17
|
+
* listed in ALLOWED_ORIGINS (comma-separated).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the canonical application URL.
|
|
22
|
+
* @returns {string} The canonical URL (no trailing slash)
|
|
23
|
+
*/
|
|
24
|
+
export function getAppUrl() {
|
|
25
|
+
const raw =
|
|
26
|
+
process.env.APP_URL ||
|
|
27
|
+
process.env.NEXTAUTH_URL ||
|
|
28
|
+
`http://localhost:${process.env.PORT || "3000"}`;
|
|
29
|
+
|
|
30
|
+
// Strip trailing slash for consistency
|
|
31
|
+
return raw.replace(/\/+$/, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns the list of allowed CORS origins.
|
|
36
|
+
*
|
|
37
|
+
* Always includes the canonical APP_URL.
|
|
38
|
+
* If ALLOWED_ORIGINS is set, those are *added* (not replacing) the canonical
|
|
39
|
+
* origin so the primary domain is never accidentally excluded.
|
|
40
|
+
*
|
|
41
|
+
* @returns {string[]} De-duplicated list of allowed origins
|
|
42
|
+
*/
|
|
43
|
+
export function getAllowedOrigins() {
|
|
44
|
+
const canonical = getAppUrl();
|
|
45
|
+
const extras = process.env.ALLOWED_ORIGINS
|
|
46
|
+
? process.env.ALLOWED_ORIGINS.split(",")
|
|
47
|
+
.map((o) => o.trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
// In development, always allow the common local ports
|
|
52
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
53
|
+
const devOrigins = isDev
|
|
54
|
+
? ["http://localhost:3000", "http://localhost:3001"]
|
|
55
|
+
: [];
|
|
56
|
+
|
|
57
|
+
// De-duplicate
|
|
58
|
+
return [...new Set([canonical, ...extras, ...devOrigins])];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ensures NEXTAUTH_URL is set in process.env so NextAuth picks it up,
|
|
63
|
+
* even when only APP_URL was configured.
|
|
64
|
+
*
|
|
65
|
+
* Call this once at startup (e.g. in your env validation step).
|
|
66
|
+
*/
|
|
67
|
+
export function syncNextAuthUrl() {
|
|
68
|
+
if (!process.env.NEXTAUTH_URL) {
|
|
69
|
+
process.env.NEXTAUTH_URL = getAppUrl();
|
|
70
|
+
}
|
|
71
|
+
}
|