@techstream/quark-create-app 1.2.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.
Files changed (73) hide show
  1. package/README.md +38 -0
  2. package/package.json +34 -0
  3. package/src/index.js +611 -0
  4. package/templates/base-project/README.md +35 -0
  5. package/templates/base-project/apps/web/next.config.js +6 -0
  6. package/templates/base-project/apps/web/package.json +32 -0
  7. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  8. package/templates/base-project/apps/web/public/file.svg +1 -0
  9. package/templates/base-project/apps/web/public/globe.svg +1 -0
  10. package/templates/base-project/apps/web/public/next.svg +1 -0
  11. package/templates/base-project/apps/web/public/vercel.svg +1 -0
  12. package/templates/base-project/apps/web/public/window.svg +1 -0
  13. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +4 -0
  14. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +39 -0
  15. package/templates/base-project/apps/web/src/app/api/csrf/route.js +42 -0
  16. package/templates/base-project/apps/web/src/app/api/error-handler.js +21 -0
  17. package/templates/base-project/apps/web/src/app/api/health/route.js +78 -0
  18. package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +61 -0
  19. package/templates/base-project/apps/web/src/app/api/posts/route.js +34 -0
  20. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  21. package/templates/base-project/apps/web/src/app/api/users/route.js +36 -0
  22. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  23. package/templates/base-project/apps/web/src/app/globals.css +26 -0
  24. package/templates/base-project/apps/web/src/app/layout.js +12 -0
  25. package/templates/base-project/apps/web/src/app/page.js +10 -0
  26. package/templates/base-project/apps/web/src/app/page.test.js +11 -0
  27. package/templates/base-project/apps/web/src/lib/auth-middleware.js +14 -0
  28. package/templates/base-project/apps/web/src/lib/auth.js +102 -0
  29. package/templates/base-project/apps/web/src/middleware.js +265 -0
  30. package/templates/base-project/apps/worker/package.json +28 -0
  31. package/templates/base-project/apps/worker/src/index.js +154 -0
  32. package/templates/base-project/apps/worker/src/index.test.js +19 -0
  33. package/templates/base-project/docker-compose.yml +40 -0
  34. package/templates/base-project/package.json +26 -0
  35. package/templates/base-project/packages/db/package.json +29 -0
  36. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +176 -0
  37. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  38. package/templates/base-project/packages/db/prisma/schema.prisma +147 -0
  39. package/templates/base-project/packages/db/prisma.config.ts +25 -0
  40. package/templates/base-project/packages/db/scripts/seed.js +47 -0
  41. package/templates/base-project/packages/db/src/client.js +52 -0
  42. package/templates/base-project/packages/db/src/generated/prisma/browser.ts +53 -0
  43. package/templates/base-project/packages/db/src/generated/prisma/client.ts +82 -0
  44. package/templates/base-project/packages/db/src/generated/prisma/commonInputTypes.ts +649 -0
  45. package/templates/base-project/packages/db/src/generated/prisma/enums.ts +19 -0
  46. package/templates/base-project/packages/db/src/generated/prisma/internal/class.ts +305 -0
  47. package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespace.ts +1428 -0
  48. package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespaceBrowser.ts +217 -0
  49. package/templates/base-project/packages/db/src/generated/prisma/models/Account.ts +2098 -0
  50. package/templates/base-project/packages/db/src/generated/prisma/models/AuditLog.ts +1805 -0
  51. package/templates/base-project/packages/db/src/generated/prisma/models/Job.ts +1737 -0
  52. package/templates/base-project/packages/db/src/generated/prisma/models/Post.ts +1762 -0
  53. package/templates/base-project/packages/db/src/generated/prisma/models/Session.ts +1738 -0
  54. package/templates/base-project/packages/db/src/generated/prisma/models/User.ts +2298 -0
  55. package/templates/base-project/packages/db/src/generated/prisma/models/VerificationToken.ts +1450 -0
  56. package/templates/base-project/packages/db/src/generated/prisma/models.ts +18 -0
  57. package/templates/base-project/packages/db/src/index.js +3 -0
  58. package/templates/base-project/packages/db/src/queries.js +267 -0
  59. package/templates/base-project/packages/db/src/queries.test.js +79 -0
  60. package/templates/base-project/packages/db/src/schemas.js +31 -0
  61. package/templates/base-project/pnpm-workspace.yaml +7 -0
  62. package/templates/base-project/turbo.json +25 -0
  63. package/templates/config/package.json +8 -0
  64. package/templates/config/src/index.js +21 -0
  65. package/templates/jobs/package.json +8 -0
  66. package/templates/jobs/src/definitions.js +9 -0
  67. package/templates/jobs/src/handlers.js +20 -0
  68. package/templates/jobs/src/index.js +2 -0
  69. package/templates/ui/package.json +11 -0
  70. package/templates/ui/src/button.js +19 -0
  71. package/templates/ui/src/card.js +14 -0
  72. package/templates/ui/src/index.js +3 -0
  73. package/templates/ui/src/input.js +11 -0
@@ -0,0 +1,35 @@
1
+ # My Quark Project
2
+
3
+ A modern, scalable monorepo built with Quark.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm dev
10
+ ```
11
+
12
+ ## Services
13
+
14
+ - **Docker**: `docker compose up -d`
15
+ - **Database**: PostgreSQL on port 5432
16
+ - **Cache**: Redis on port 6379
17
+ - **Email**: Mailhog UI on port 8025
18
+
19
+ ## Development
20
+
21
+ ```bash
22
+ # Build all packages
23
+ pnpm build
24
+
25
+ # Run tests
26
+ pnpm test
27
+
28
+ # Lint
29
+ pnpm lint
30
+ ```
31
+
32
+ ## Structure
33
+
34
+ - `apps/` - Applications (web, worker, etc.)
35
+ - `packages/` - Shared packages (ui, jobs, config, core, db, cli)
@@ -0,0 +1,6 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ // No TypeScript configuration needed for JS-only projects
4
+ };
5
+
6
+ export default nextConfig;
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@quark/web",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "private": true,
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "test": "node --test $(find src -name '*.test.js')",
11
+ "lint": "biome format --write && biome check --write"
12
+ },
13
+ "dependencies": {
14
+ "@auth/prisma-adapter": "^2.11.1",
15
+ "@techstream/quark-core": "^1.0.0",
16
+ "@techstream/quark-db": "workspace:*",
17
+ "@techstream/quark-jobs": "workspace:*",
18
+ "@techstream/quark-ui": "workspace:*",
19
+ "@prisma/client": "^7.3.0",
20
+ "next": "16.1.6",
21
+ "next-auth": "5.0.0-beta.30",
22
+ "pg": "^8.18.0",
23
+ "react": "19.2.0",
24
+ "react-dom": "19.2.0"
25
+ },
26
+ "devDependencies": {
27
+ "@techstream/quark-config": "workspace:*",
28
+ "@tailwindcss/postcss": "^4",
29
+ "@types/node": "^20",
30
+ "tailwindcss": "^4"
31
+ }
32
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,4 @@
1
+ import { handlers } from "@/lib/auth";
2
+
3
+ export const GET = (req, context) => handlers.GET(req, context);
4
+ export const POST = (req, context) => handlers.POST(req, context);
@@ -0,0 +1,39 @@
1
+ import { hashPassword, validateBody } from "@techstream/quark-core";
2
+ import { user, userRegisterSchema } from "@techstream/quark-db";
3
+ import { NextResponse } from "next/server";
4
+ import { handleError } from "../../error-handler";
5
+
6
+ export async function POST(request) {
7
+ try {
8
+ const data = await validateBody(request, userRegisterSchema);
9
+
10
+ // Check if user exists
11
+ const existing = await user.findByEmail(data.email);
12
+ if (existing) {
13
+ return NextResponse.json(
14
+ { message: "User with this email already exists" },
15
+ { status: 409 },
16
+ );
17
+ }
18
+
19
+ const hashedPassword = await hashPassword(data.password);
20
+
21
+ // Remove password from data before passing to create (create expects plain data object, but we need to inject hashed password)
22
+ // We need to update the user.create method or pass it manually.
23
+ // The current user.create just takes 'data'.
24
+
25
+ // Prisma create data:
26
+ const newUser = await user.create({
27
+ email: data.email,
28
+ name: data.name,
29
+ password: hashedPassword,
30
+ });
31
+
32
+ // Don't return the password
33
+ const { password: _, ...safeUser } = newUser;
34
+
35
+ return NextResponse.json(safeUser, { status: 201 });
36
+ } catch (error) {
37
+ return handleError(error);
38
+ }
39
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * CSRF Token API Endpoint
3
+ * Generates a CSRF token, stores it in a secure HTTP-only cookie,
4
+ * and returns it to the client for inclusion in request headers.
5
+ */
6
+
7
+ import { generateCsrfToken } from "@techstream/quark-core";
8
+ import { NextResponse } from "next/server";
9
+ import { auth } from "@/lib/auth";
10
+
11
+ export async function GET() {
12
+ try {
13
+ const session = await auth();
14
+
15
+ if (!session) {
16
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
17
+ }
18
+
19
+ // Generate CSRF token
20
+ const csrfToken = generateCsrfToken();
21
+
22
+ const response = NextResponse.json({ csrfToken });
23
+
24
+ // Store the token in a secure HTTP-only cookie so the server can
25
+ // validate it on subsequent state-changing requests.
26
+ const isProduction = process.env.NODE_ENV === "production";
27
+ response.cookies.set("csrf_token", csrfToken, {
28
+ httpOnly: true,
29
+ secure: isProduction,
30
+ sameSite: "strict",
31
+ path: "/",
32
+ maxAge: 60 * 60, // 1 hour
33
+ });
34
+
35
+ return response;
36
+ } catch (_error) {
37
+ return NextResponse.json(
38
+ { error: "Failed to generate CSRF token" },
39
+ { status: 500 },
40
+ );
41
+ }
42
+ }
@@ -0,0 +1,21 @@
1
+ import { AppError } from "@techstream/quark-core";
2
+ import { NextResponse } from "next/server";
3
+
4
+ export function handleError(error) {
5
+ console.error("API Error:", error);
6
+
7
+ if (error instanceof AppError) {
8
+ return NextResponse.json(error.toJSON(), { status: error.statusCode });
9
+ }
10
+
11
+ // Fallback for unhandled errors
12
+ return NextResponse.json(
13
+ {
14
+ name: "InternalServerError",
15
+ message: "An unexpected error occurred",
16
+ statusCode: 500,
17
+ code: "INTERNAL_ERROR",
18
+ },
19
+ { status: 500 },
20
+ );
21
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Health Check Endpoint
3
+ * Verifies the application and its dependencies are functioning correctly.
4
+ * Times out after 5 seconds to prevent hanging.
5
+ */
6
+
7
+ import { pingRedis } from "@techstream/quark-core";
8
+ import { prisma } from "@techstream/quark-db";
9
+ import { NextResponse } from "next/server";
10
+
11
+ /** Overall timeout for the health check (ms). */
12
+ const HEALTH_CHECK_TIMEOUT = 5000;
13
+
14
+ export async function GET() {
15
+ try {
16
+ const result = await Promise.race([
17
+ runHealthChecks(),
18
+ new Promise((_, reject) =>
19
+ setTimeout(
20
+ () => reject(new Error("Health check timed out")),
21
+ HEALTH_CHECK_TIMEOUT,
22
+ ),
23
+ ),
24
+ ]);
25
+
26
+ return NextResponse.json(result, {
27
+ status: result.status === "ok" ? 200 : 503,
28
+ });
29
+ } catch (error) {
30
+ console.error("Health check failed:", error);
31
+ return NextResponse.json(
32
+ {
33
+ status: "error",
34
+ timestamp: new Date().toISOString(),
35
+ message: error.message,
36
+ },
37
+ { status: 500 },
38
+ );
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Runs all individual health checks and aggregates the results.
44
+ * @returns {Promise<{ status: string, timestamp: string, checks: Record<string, object> }>}
45
+ */
46
+ async function runHealthChecks() {
47
+ const health = {
48
+ status: "ok",
49
+ timestamp: new Date().toISOString(),
50
+ checks: {},
51
+ };
52
+
53
+ // Check database connectivity
54
+ try {
55
+ await prisma.$queryRaw`SELECT 1`;
56
+ health.checks.database = { status: "ok" };
57
+ } catch (error) {
58
+ health.checks.database = {
59
+ status: "error",
60
+ message: error.message,
61
+ };
62
+ health.status = "degraded";
63
+ }
64
+
65
+ // Check Redis connectivity (actual PING)
66
+ const redisResult = await pingRedis();
67
+ if (redisResult.status === "ok") {
68
+ health.checks.redis = { status: "ok", latencyMs: redisResult.latencyMs };
69
+ } else {
70
+ health.checks.redis = {
71
+ status: "error",
72
+ message: redisResult.message,
73
+ };
74
+ health.status = "degraded";
75
+ }
76
+
77
+ return health;
78
+ }
@@ -0,0 +1,61 @@
1
+ import { UnauthorizedError, validateBody } from "@techstream/quark-core";
2
+ import { post, postUpdateSchema } from "@techstream/quark-db";
3
+ import { NextResponse } from "next/server";
4
+ import { requireAuth } from "@/lib/auth-middleware";
5
+ import { handleError } from "../../error-handler";
6
+
7
+ export async function GET(_request, { params }) {
8
+ try {
9
+ const { id } = await params;
10
+ const foundPost = await post.findById(id);
11
+ if (!foundPost) {
12
+ return NextResponse.json({ message: "Post not found" }, { status: 404 });
13
+ }
14
+ return NextResponse.json(foundPost);
15
+ } catch (error) {
16
+ return handleError(error);
17
+ }
18
+ }
19
+
20
+ export async function PATCH(request, { params }) {
21
+ try {
22
+ const session = await requireAuth();
23
+ const { id } = await params;
24
+
25
+ const foundPost = await post.findById(id);
26
+ if (!foundPost) {
27
+ return NextResponse.json({ message: "Post not found" }, { status: 404 });
28
+ }
29
+
30
+ if (foundPost.authorId !== session.user.id) {
31
+ throw new UnauthorizedError("You can only edit your own posts");
32
+ }
33
+
34
+ const data = await validateBody(request, postUpdateSchema);
35
+ const updatedPost = await post.update(id, data);
36
+ return NextResponse.json(updatedPost);
37
+ } catch (error) {
38
+ return handleError(error);
39
+ }
40
+ }
41
+
42
+ export async function DELETE(_request, { params }) {
43
+ try {
44
+ const session = await requireAuth();
45
+ const { id } = await params;
46
+
47
+ const foundPost = await post.findById(id);
48
+ if (!foundPost) {
49
+ return NextResponse.json({ message: "Post not found" }, { status: 404 });
50
+ }
51
+
52
+ if (foundPost.authorId !== session.user.id) {
53
+ throw new UnauthorizedError("You can only delete your own posts");
54
+ }
55
+
56
+ await post.delete(id);
57
+ return NextResponse.json({ success: true });
58
+ } catch (error) {
59
+ return handleError(error);
60
+ }
61
+ }
@@ -0,0 +1,34 @@
1
+ import { validateBody } from "@techstream/quark-core";
2
+ import { post, postCreateSchema } from "@techstream/quark-db";
3
+ import { NextResponse } from "next/server";
4
+ import { requireAuth } from "@/lib/auth-middleware";
5
+ import { handleError } from "../error-handler";
6
+
7
+ export async function GET(request) {
8
+ try {
9
+ const { searchParams } = new URL(request.url);
10
+ const page = parseInt(searchParams.get("page") || "1", 10);
11
+ const limit = parseInt(searchParams.get("limit") || "10", 10);
12
+ const skip = (page - 1) * limit;
13
+
14
+ const posts = await post.findAll({ skip, take: limit });
15
+ return NextResponse.json(posts);
16
+ } catch (error) {
17
+ return handleError(error);
18
+ }
19
+ }
20
+
21
+ export async function POST(request) {
22
+ try {
23
+ const session = await requireAuth();
24
+ const data = await validateBody(request, postCreateSchema);
25
+
26
+ const newPost = await post.create({
27
+ ...data,
28
+ authorId: session.user.id,
29
+ });
30
+ return NextResponse.json(newPost, { status: 201 });
31
+ } catch (error) {
32
+ return handleError(error);
33
+ }
34
+ }
@@ -0,0 +1,54 @@
1
+ import { validateBody } from "@techstream/quark-core";
2
+ import { user, userUpdateSchema } from "@techstream/quark-db";
3
+ import { NextResponse } from "next/server";
4
+ import { requireAuth } from "@/lib/auth-middleware";
5
+ import { handleError } from "../../error-handler";
6
+
7
+ export async function GET(_request, { params }) {
8
+ try {
9
+ await requireAuth();
10
+ const { id } = await params;
11
+ const foundUser = await user.findById(id);
12
+ if (!foundUser) {
13
+ return NextResponse.json({ message: "User not found" }, { status: 404 });
14
+ }
15
+ return NextResponse.json(foundUser);
16
+ } catch (error) {
17
+ return handleError(error);
18
+ }
19
+ }
20
+
21
+ export async function PATCH(request, { params }) {
22
+ try {
23
+ await requireAuth();
24
+ const { id } = await params;
25
+
26
+ const existingUser = await user.findById(id);
27
+ if (!existingUser) {
28
+ return NextResponse.json({ message: "User not found" }, { status: 404 });
29
+ }
30
+
31
+ const data = await validateBody(request, userUpdateSchema);
32
+ const updatedUser = await user.update(id, data);
33
+ return NextResponse.json(updatedUser);
34
+ } catch (error) {
35
+ return handleError(error);
36
+ }
37
+ }
38
+
39
+ export async function DELETE(_request, { params }) {
40
+ try {
41
+ await requireAuth();
42
+ const { id } = await params;
43
+
44
+ const existingUser = await user.findById(id);
45
+ if (!existingUser) {
46
+ return NextResponse.json({ message: "User not found" }, { status: 404 });
47
+ }
48
+
49
+ await user.delete(id);
50
+ return NextResponse.json({ success: true });
51
+ } catch (error) {
52
+ return handleError(error);
53
+ }
54
+ }
@@ -0,0 +1,36 @@
1
+ import { validateBody } from "@techstream/quark-core";
2
+ import { user, userCreateSchema } from "@techstream/quark-db";
3
+ import { NextResponse } from "next/server";
4
+ import { requireAuth } from "@/lib/auth-middleware";
5
+ import { handleError } from "../error-handler";
6
+
7
+ export async function GET(_request) {
8
+ try {
9
+ await requireAuth();
10
+ const users = await user.findAll();
11
+ return NextResponse.json(users);
12
+ } catch (error) {
13
+ return handleError(error);
14
+ }
15
+ }
16
+
17
+ export async function POST(request) {
18
+ try {
19
+ await requireAuth();
20
+ const data = await validateBody(request, userCreateSchema);
21
+
22
+ // Check if email already exists
23
+ const existing = await user.findByEmail(data.email);
24
+ if (existing) {
25
+ return NextResponse.json(
26
+ { message: "User with this email already exists" },
27
+ { status: 409 },
28
+ );
29
+ }
30
+
31
+ const newUser = await user.create(data);
32
+ return NextResponse.json(newUser, { status: 201 });
33
+ } catch (error) {
34
+ return handleError(error);
35
+ }
36
+ }
@@ -0,0 +1,26 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #ffffff;
5
+ --foreground: #171717;
6
+ }
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans: var(--font-geist-sans);
12
+ --font-mono: var(--font-geist-mono);
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --background: #0a0a0a;
18
+ --foreground: #ededed;
19
+ }
20
+ }
21
+
22
+ body {
23
+ background: var(--background);
24
+ color: var(--foreground);
25
+ font-family: Arial, Helvetica, sans-serif;
26
+ }
@@ -0,0 +1,12 @@
1
+ export const metadata = {
2
+ title: "Quark",
3
+ description: "A modern monorepo with Next.js, React, and Prisma",
4
+ };
5
+
6
+ export default function RootLayout({ children }) {
7
+ return (
8
+ <html lang="en">
9
+ <body>{children}</body>
10
+ </html>
11
+ );
12
+ }
@@ -0,0 +1,10 @@
1
+ export default function Home() {
2
+ return (
3
+ <main className="min-h-screen flex flex-col items-center justify-center p-8">
4
+ <h1 className="text-4xl font-bold text-center mb-4">Quark</h1>
5
+ <p className="text-lg text-gray-600 text-center max-w-md">
6
+ Welcome to the Quark monorepo. This is a Next.js 16 app with React 19.
7
+ </p>
8
+ </main>
9
+ );
10
+ }
@@ -0,0 +1,11 @@
1
+ import assert from "node:assert";
2
+ import { test } from "node:test";
3
+
4
+ test("Home Page - should have Quark defined", () => {
5
+ assert.ok("Quark");
6
+ });
7
+
8
+ test("Home Page - page module exists", () => {
9
+ // Basic smoke test - the actual page is tested via E2E tests
10
+ assert.strictEqual(true, true);
11
+ });
@@ -0,0 +1,14 @@
1
+ import { UnauthorizedError } from "@techstream/quark-core";
2
+ import { auth } from "./auth";
3
+
4
+ export async function requireAuth() {
5
+ const session = await auth();
6
+
7
+ if (!session) {
8
+ throw new UnauthorizedError(
9
+ "You must be logged in to access this resource",
10
+ );
11
+ }
12
+
13
+ return session;
14
+ }
@@ -0,0 +1,102 @@
1
+ import { PrismaAdapter } from "@auth/prisma-adapter";
2
+ import { createAuthConfig, verifyPassword } from "@techstream/quark-core";
3
+ import { prisma, user } from "@techstream/quark-db";
4
+ import NextAuth from "next-auth";
5
+ import CredentialsProvider from "next-auth/providers/credentials";
6
+ import GithubProvider from "next-auth/providers/github";
7
+ import GoogleProvider from "next-auth/providers/google";
8
+
9
+ const providers = [
10
+ CredentialsProvider({
11
+ name: "Credentials",
12
+ credentials: {
13
+ email: { label: "Email", type: "email" },
14
+ password: { label: "Password", type: "password" },
15
+ },
16
+ async authorize(credentials) {
17
+ if (!credentials?.email || !credentials?.password) {
18
+ return null;
19
+ }
20
+
21
+ const existingUser = await user.findByEmail(credentials.email);
22
+
23
+ if (!existingUser || !existingUser.password) {
24
+ return null;
25
+ }
26
+
27
+ const isValid = await verifyPassword(
28
+ credentials.password,
29
+ existingUser.password,
30
+ );
31
+
32
+ if (!isValid) {
33
+ return null;
34
+ }
35
+
36
+ return {
37
+ id: existingUser.id,
38
+ email: existingUser.email,
39
+ name: existingUser.name,
40
+ image: existingUser.image,
41
+ role: existingUser.role,
42
+ };
43
+ },
44
+ }),
45
+ ];
46
+
47
+ if (process.env.GITHUB_ID && process.env.GITHUB_SECRET) {
48
+ providers.push(
49
+ GithubProvider({
50
+ clientId: process.env.GITHUB_ID,
51
+ clientSecret: process.env.GITHUB_SECRET,
52
+ }),
53
+ );
54
+ }
55
+
56
+ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
57
+ providers.push(
58
+ GoogleProvider({
59
+ clientId: process.env.GOOGLE_CLIENT_ID,
60
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
61
+ }),
62
+ );
63
+ }
64
+
65
+ export function getAuthOptions() {
66
+ return createAuthConfig({
67
+ adapter: PrismaAdapter(prisma),
68
+ providers: providers,
69
+ session: {
70
+ strategy: "jwt",
71
+ },
72
+ });
73
+ }
74
+
75
+ let authInstance = null;
76
+
77
+ function getAuthInstance() {
78
+ if (!authInstance) {
79
+ authInstance = NextAuth(getAuthOptions());
80
+ }
81
+ return authInstance;
82
+ }
83
+
84
+ export function getAuth() {
85
+ return getAuthInstance();
86
+ }
87
+
88
+ export async function auth() {
89
+ return getAuthInstance().auth();
90
+ }
91
+
92
+ export const handlers = new Proxy(
93
+ {},
94
+ {
95
+ get(_target, prop) {
96
+ return getAuthInstance().handlers[prop];
97
+ },
98
+ },
99
+ );
100
+
101
+ export const signIn = (...args) => getAuthInstance().signIn(...args);
102
+ export const signOut = (...args) => getAuthInstance().signOut(...args);