@usequark/quark-create-app 1.25.8 → 1.25.9
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 +1 -1
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +39 -1
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +7 -12
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +2 -1
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/error-handler.js +1 -1
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/files/route.js +6 -4
- package/templates/base-project/apps/web/src/app/api/health/route.js +4 -4
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/users/route.js +1 -1
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +1 -1
- package/templates/base-project/apps/web/src/lib/auth.js +2 -5
- package/templates/base-project/apps/web/src/lib/enqueue-welcome-email.js +51 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +2 -1
- package/templates/base-project/apps/web/src/preflight.js +1 -1
- package/templates/base-project/biome.json +1 -1
- package/templates/base-project/packages/db/src/umami.js +1 -1
- package/templates/config/src/redis.js +1 -1
- package/templates/config/src/validate-env.js +1 -1
- package/templates/worker/src/handlers/email.js +2 -2
- package/templates/worker/src/handlers/files.js +1 -1
- package/templates/worker/src/index.js +4 -4
- package/templates/worker/src/lib/railway.js +1 -1
- package/templates/worker/src/lib/tokens.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { getAppleClientIds } from "@usequark/quark-config/oauth";
|
|
3
|
-
import { validateBody } from "@usequark/quark-core";
|
|
3
|
+
import { validateBody } from "@usequark/quark-core/core";
|
|
4
4
|
import { user } from "@usequark/quark-db";
|
|
5
5
|
import { importJWK, jwtVerify } from "jose";
|
|
6
6
|
import { NextResponse } from "next/server";
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
getGoogleClientId,
|
|
3
3
|
isAudienceValid,
|
|
4
4
|
} from "@usequark/quark-config/oauth";
|
|
5
|
-
import { validateBody } from "@usequark/quark-core";
|
|
5
|
+
import { validateBody } from "@usequark/quark-core/core";
|
|
6
6
|
import { user } from "@usequark/quark-db";
|
|
7
7
|
import { NextResponse } from "next/server";
|
|
8
8
|
import { z } from "zod";
|
|
@@ -15,6 +15,26 @@ const googleAuthSchema = z.object({
|
|
|
15
15
|
|
|
16
16
|
const GOOGLE_TOKENINFO_URL = "https://oauth2.googleapis.com/tokeninfo";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Whether Google's `email_verified` claim says the address is confirmed.
|
|
20
|
+
*
|
|
21
|
+
* `tokeninfo` returns the claim as the *string* `"true"` or `"false"`, not a
|
|
22
|
+
* boolean, so a plain truthiness check would do the exact opposite of the
|
|
23
|
+
* obvious thing: reject every real sign-in (`"false"` is truthy) while waving
|
|
24
|
+
* through the one case that matters. Only an explicit confirmation counts.
|
|
25
|
+
* Anything else — a missing claim, a number, a null — is unverified, because
|
|
26
|
+
* the failure modes here are asymmetric: a wrong refusal costs one unlucky
|
|
27
|
+
* sign-in, and a wrong acceptance hands an unconfirmed address a session.
|
|
28
|
+
*
|
|
29
|
+
* @param {unknown} value
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
function isEmailVerified(value) {
|
|
33
|
+
if (typeof value === "boolean") return value;
|
|
34
|
+
if (typeof value !== "string") return false;
|
|
35
|
+
return value.trim().toLowerCase() === "true";
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/**
|
|
19
39
|
* POST /api/auth/google
|
|
20
40
|
* Verify Google ID token and issue a custom JWT.
|
|
@@ -73,6 +93,24 @@ export async function POST(request) {
|
|
|
73
93
|
);
|
|
74
94
|
}
|
|
75
95
|
|
|
96
|
+
// An unverified address proves nothing about who controls the inbox, and
|
|
97
|
+
// this route creates the account on first sight — so without this check the
|
|
98
|
+
// confirmation that is supposed to establish ownership of the address is
|
|
99
|
+
// the attacker's to skip rather than the victim's to perform.
|
|
100
|
+
//
|
|
101
|
+
// It sits after the missing-email `400` on purpose. That one describes the
|
|
102
|
+
// shape of the token rather than reporting that the token was refused, and
|
|
103
|
+
// putting this check ahead of it would change a response operators already
|
|
104
|
+
// read for what it is. What matters for security is the position relative
|
|
105
|
+
// to the database: both checks land before `findByEmail`, so an unverified
|
|
106
|
+
// address can never become a registration or existence oracle.
|
|
107
|
+
if (!isEmailVerified(tokenInfo.email_verified)) {
|
|
108
|
+
return NextResponse.json(
|
|
109
|
+
{ message: "Invalid Google token" },
|
|
110
|
+
{ status: 401 },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
76
114
|
// Find or create user
|
|
77
115
|
let existingUser = await user.findByEmail(googleEmail);
|
|
78
116
|
if (!existingUser) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createLogger, validateBody } from "@usequark/quark-core";
|
|
1
|
+
import { createLogger, validateBody } from "@usequark/quark-core/core";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { issueTokenPair, verifyMobileToken } from "../../../../lib/jwt";
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
hashPassword,
|
|
4
|
-
validateBody,
|
|
5
|
-
withCsrfProtection,
|
|
6
|
-
} from "@usequark/quark-core";
|
|
1
|
+
import { hashPassword } from "@usequark/quark-core/auth";
|
|
2
|
+
import { validateBody, withCsrfProtection } from "@usequark/quark-core/core";
|
|
7
3
|
import { user, userRegisterSchema } from "@usequark/quark-db";
|
|
8
|
-
import { JOB_NAMES, JOB_QUEUES } from "@usequark/quark-jobs";
|
|
9
4
|
import { NextResponse } from "next/server";
|
|
10
5
|
import { isSignupEnabled } from "@/lib/auth-signup";
|
|
6
|
+
import { enqueueWelcomeEmail } from "@/lib/enqueue-welcome-email";
|
|
11
7
|
import { handleError } from "../../error-handler";
|
|
12
8
|
|
|
13
9
|
export const POST = withCsrfProtection(async (request) => {
|
|
@@ -46,12 +42,11 @@ export const POST = withCsrfProtection(async (request) => {
|
|
|
46
42
|
// Don't return the password
|
|
47
43
|
const { password: _, ...safeUser } = newUser;
|
|
48
44
|
|
|
49
|
-
// Enqueue welcome email (fire-and-forget - don't block the response)
|
|
45
|
+
// Enqueue welcome email (fire-and-forget - don't block the response).
|
|
46
|
+
// The queue module is reached through a lazily-loaded helper, so BullMQ
|
|
47
|
+
// stays out of this process until a registration actually happens.
|
|
50
48
|
try {
|
|
51
|
-
|
|
52
|
-
await emailQueue.add(JOB_NAMES.SEND_WELCOME_EMAIL, {
|
|
53
|
-
userId: newUser.id,
|
|
54
|
-
});
|
|
49
|
+
await enqueueWelcomeEmail(newUser.id);
|
|
55
50
|
} catch {
|
|
56
51
|
// Non-critical - user is created even if email fails to enqueue
|
|
57
52
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { verifyPassword } from "@usequark/quark-core/auth";
|
|
2
|
+
import { validateBody } from "@usequark/quark-core/core";
|
|
2
3
|
import { user } from "@usequark/quark-db";
|
|
3
4
|
import { NextResponse } from "next/server";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and returns it to the client for inclusion in request headers.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { generateCsrfToken } from "@usequark/quark-core";
|
|
7
|
+
import { generateCsrfToken } from "@usequark/quark-core/core";
|
|
8
8
|
import { NextResponse } from "next/server";
|
|
9
9
|
import { getCsrfCookieOptions } from "@/lib/csrf-cookie";
|
|
10
10
|
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
import {
|
|
8
8
|
ConflictError,
|
|
9
9
|
createLogger,
|
|
10
|
-
createStorage,
|
|
11
10
|
ForbiddenError,
|
|
12
11
|
NotFoundError,
|
|
13
12
|
withCsrfProtection,
|
|
14
|
-
} from "@usequark/quark-core";
|
|
13
|
+
} from "@usequark/quark-core/core";
|
|
14
|
+
import { createStorage } from "@usequark/quark-core/storage";
|
|
15
15
|
import { file } from "@usequark/quark-db";
|
|
16
16
|
import { NextResponse } from "next/server";
|
|
17
17
|
import { requireAuth } from "@/lib/auth-middleware";
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
createStorage,
|
|
9
|
-
generateStorageKey,
|
|
10
|
-
parseMultipart,
|
|
11
8
|
parsePaginationQuery,
|
|
12
9
|
validateFile,
|
|
13
10
|
withCsrfProtection,
|
|
14
|
-
} from "@usequark/quark-core";
|
|
11
|
+
} from "@usequark/quark-core/core";
|
|
12
|
+
import { parseMultipart } from "@usequark/quark-core/multipart";
|
|
13
|
+
import {
|
|
14
|
+
createStorage,
|
|
15
|
+
generateStorageKey,
|
|
16
|
+
} from "@usequark/quark-core/storage";
|
|
15
17
|
import { file } from "@usequark/quark-db";
|
|
16
18
|
import { NextResponse } from "next/server";
|
|
17
19
|
import { requireAuth } from "@/lib/auth-middleware";
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
* what the core module replaces.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { createLogger } from "@usequark/quark-core/core";
|
|
20
21
|
import {
|
|
21
22
|
checkQueues,
|
|
22
23
|
checkStorage,
|
|
23
|
-
createLogger,
|
|
24
|
-
getRegisteredQueues,
|
|
25
|
-
pingRedis,
|
|
26
24
|
runHealthChecks,
|
|
27
|
-
} from "@usequark/quark-core";
|
|
25
|
+
} from "@usequark/quark-core/health";
|
|
26
|
+
import { getRegisteredQueues } from "@usequark/quark-core/queue";
|
|
27
|
+
import { pingRedis } from "@usequark/quark-core/redis";
|
|
28
28
|
import { pingDatabase } from "@usequark/quark-db";
|
|
29
29
|
import { NextResponse } from "next/server";
|
|
30
30
|
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
ConflictError,
|
|
3
3
|
validateBody,
|
|
4
4
|
withCsrfProtection,
|
|
5
|
-
} from "@usequark/quark-core";
|
|
5
|
+
} from "@usequark/quark-core/core";
|
|
6
6
|
import { user, userUpdateSchema } from "@usequark/quark-db";
|
|
7
7
|
import { NextResponse } from "next/server";
|
|
8
8
|
import { requireRole } from "@/lib/auth-middleware";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validateBody, withCsrfProtection } from "@usequark/quark-core";
|
|
1
|
+
import { validateBody, withCsrfProtection } from "@usequark/quark-core/core";
|
|
2
2
|
import { user, userUpdateSchema } from "@usequark/quark-db";
|
|
3
3
|
import { NextResponse } from "next/server";
|
|
4
4
|
import { extractBearerPayload } from "../../../../lib/jwt";
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
parsePaginationQuery,
|
|
3
3
|
validateBody,
|
|
4
4
|
withCsrfProtection,
|
|
5
|
-
} from "@usequark/quark-core";
|
|
5
|
+
} from "@usequark/quark-core/core";
|
|
6
6
|
import { user, userCreateSchema } from "@usequark/quark-db";
|
|
7
7
|
import { NextResponse } from "next/server";
|
|
8
8
|
import { requireRole } from "@/lib/auth-middleware";
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { PrismaAdapter } from "@auth/prisma-adapter";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
createLogger,
|
|
5
|
-
verifyPassword,
|
|
6
|
-
} from "@usequark/quark-core";
|
|
2
|
+
import { createAuthConfig, verifyPassword } from "@usequark/quark-core/auth";
|
|
3
|
+
import { createLogger } from "@usequark/quark-core/core";
|
|
7
4
|
import { Prisma, prisma, user } from "@usequark/quark-db";
|
|
8
5
|
import NextAuth from "next-auth";
|
|
9
6
|
import CredentialsProvider from "next-auth/providers/credentials";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { JOB_NAMES, JOB_QUEUES } from "@usequark/quark-jobs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loads the queue module on demand.
|
|
5
|
+
*
|
|
6
|
+
* Kept as a named binding rather than an inline `import()` so it can be
|
|
7
|
+
* substituted — see `loadQueue` on {@link enqueueWelcomeEmail}.
|
|
8
|
+
*
|
|
9
|
+
* @returns {Promise<typeof import("@usequark/quark-core/queue")>}
|
|
10
|
+
*/
|
|
11
|
+
function loadQueueModule() {
|
|
12
|
+
return import("@usequark/quark-core/queue");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enqueues the post-signup welcome email for a newly created user.
|
|
17
|
+
*
|
|
18
|
+
* This lives in its own module for two reasons.
|
|
19
|
+
*
|
|
20
|
+
* The queue module is imported dynamically from inside this file, so callers
|
|
21
|
+
* that import *this* statically never pull BullMQ into their module graph. That
|
|
22
|
+
* matters most for `/api/auth/register`, which is reachable on the web service:
|
|
23
|
+
* a module-scope `createQueue` import both loaded BullMQ at startup and pinned a
|
|
24
|
+
* Redis connection to the web process for as long as the process lived.
|
|
25
|
+
*
|
|
26
|
+
* It also gives the route a static boundary to substitute in tests. `mock.module`
|
|
27
|
+
* intercepts a dynamic import only when the importing module registered the mock,
|
|
28
|
+
* so a dynamic import made by the route itself cannot be mocked from the route's
|
|
29
|
+
* test. A static import of this module can.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} userId - The user to welcome. The worker rejects a job with
|
|
32
|
+
* no userId, so this is required rather than defaulted.
|
|
33
|
+
* @param {() => Promise<{ createQueue: (name: string, options?: object) => object }>} [loadQueue]
|
|
34
|
+
* How to obtain the queue module. Defaults to the dynamic import above;
|
|
35
|
+
* overridden in tests.
|
|
36
|
+
* @returns {Promise<void>}
|
|
37
|
+
* @throws Whatever `createQueue` or `add` throws. The queue is closed either
|
|
38
|
+
* way, so a caller that swallows the error still leaves no connection behind.
|
|
39
|
+
*/
|
|
40
|
+
export async function enqueueWelcomeEmail(userId, loadQueue = loadQueueModule) {
|
|
41
|
+
const { createQueue } = await loadQueue();
|
|
42
|
+
const queue = createQueue(JOB_QUEUES.EMAIL);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await queue.add(JOB_NAMES.SEND_WELCOME_EMAIL, { userId });
|
|
46
|
+
} finally {
|
|
47
|
+
// Never let a failing close mask the enqueue outcome — the caller only
|
|
48
|
+
// needs to know whether the job reached the queue.
|
|
49
|
+
await queue.close().catch(() => {});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { loadConfig } from "@usequark/quark-config";
|
|
4
|
-
import { createLogger } from "@usequark/quark-core";
|
|
4
|
+
import { createLogger } from "@usequark/quark-core/core";
|
|
5
5
|
import { pingDatabase } from "@usequark/quark-db";
|
|
6
6
|
|
|
7
7
|
const logger = createLogger("web-preflight");
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Processes email-related background jobs
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { createEmailService } from "@usequark/quark-core/email";
|
|
6
7
|
import {
|
|
7
|
-
createEmailService,
|
|
8
8
|
passwordResetEmail,
|
|
9
9
|
welcomeEmail,
|
|
10
|
-
} from "@usequark/quark-core";
|
|
10
|
+
} from "@usequark/quark-core/email-templates";
|
|
11
11
|
import { prisma } from "@usequark/quark-db";
|
|
12
12
|
import { JOB_NAMES } from "@usequark/quark-jobs";
|
|
13
13
|
import {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Processes file-related background jobs (cleanup, etc.)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createStorage } from "@usequark/quark-core";
|
|
6
|
+
import { createStorage } from "@usequark/quark-core/storage";
|
|
7
7
|
import { file } from "@usequark/quark-db";
|
|
8
8
|
import { JOB_NAMES } from "@usequark/quark-jobs";
|
|
9
9
|
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
* Handles job execution, retries, and error tracking
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { createLogger } from "@usequark/quark-core/core";
|
|
8
|
+
import { AppError } from "@usequark/quark-core/errors";
|
|
7
9
|
import {
|
|
8
10
|
checkQueueHealth,
|
|
9
|
-
createLogger,
|
|
10
11
|
createQueue,
|
|
11
12
|
createWorker,
|
|
12
|
-
getRedisEndpoint,
|
|
13
13
|
updateQueueDepths,
|
|
14
|
-
} from "@usequark/quark-core";
|
|
15
|
-
import {
|
|
14
|
+
} from "@usequark/quark-core/queue";
|
|
15
|
+
import { getRedisEndpoint, getRedisUrl } from "@usequark/quark-core/redis";
|
|
16
16
|
import { job, prisma } from "@usequark/quark-db";
|
|
17
17
|
import { JOB_NAMES, JOB_QUEUES } from "@usequark/quark-jobs";
|
|
18
18
|
import { jobHandlers } from "./handlers/index.js";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Fetches estimated usage and deployment data from Railway's GraphQL API.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createLogger } from "@usequark/quark-core";
|
|
6
|
+
import { createLogger } from "@usequark/quark-core/core";
|
|
7
7
|
import { AppError } from "@usequark/quark-core/errors";
|
|
8
8
|
|
|
9
9
|
const log = createLogger("lib:railway");
|