@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.
- package/README.md +38 -0
- package/package.json +34 -0
- package/src/index.js +611 -0
- package/templates/base-project/README.md +35 -0
- package/templates/base-project/apps/web/next.config.js +6 -0
- package/templates/base-project/apps/web/package.json +32 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/file.svg +1 -0
- package/templates/base-project/apps/web/public/globe.svg +1 -0
- package/templates/base-project/apps/web/public/next.svg +1 -0
- package/templates/base-project/apps/web/public/vercel.svg +1 -0
- package/templates/base-project/apps/web/public/window.svg +1 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +4 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +39 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +42 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +21 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +78 -0
- package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +61 -0
- package/templates/base-project/apps/web/src/app/api/posts/route.js +34 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +36 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/globals.css +26 -0
- package/templates/base-project/apps/web/src/app/layout.js +12 -0
- package/templates/base-project/apps/web/src/app/page.js +10 -0
- package/templates/base-project/apps/web/src/app/page.test.js +11 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +14 -0
- package/templates/base-project/apps/web/src/lib/auth.js +102 -0
- package/templates/base-project/apps/web/src/middleware.js +265 -0
- package/templates/base-project/apps/worker/package.json +28 -0
- package/templates/base-project/apps/worker/src/index.js +154 -0
- package/templates/base-project/apps/worker/src/index.test.js +19 -0
- package/templates/base-project/docker-compose.yml +40 -0
- package/templates/base-project/package.json +26 -0
- package/templates/base-project/packages/db/package.json +29 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +176 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +147 -0
- package/templates/base-project/packages/db/prisma.config.ts +25 -0
- package/templates/base-project/packages/db/scripts/seed.js +47 -0
- package/templates/base-project/packages/db/src/client.js +52 -0
- package/templates/base-project/packages/db/src/generated/prisma/browser.ts +53 -0
- package/templates/base-project/packages/db/src/generated/prisma/client.ts +82 -0
- package/templates/base-project/packages/db/src/generated/prisma/commonInputTypes.ts +649 -0
- package/templates/base-project/packages/db/src/generated/prisma/enums.ts +19 -0
- package/templates/base-project/packages/db/src/generated/prisma/internal/class.ts +305 -0
- package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespace.ts +1428 -0
- package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespaceBrowser.ts +217 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/Account.ts +2098 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/AuditLog.ts +1805 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/Job.ts +1737 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/Post.ts +1762 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/Session.ts +1738 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/User.ts +2298 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/VerificationToken.ts +1450 -0
- package/templates/base-project/packages/db/src/generated/prisma/models.ts +18 -0
- package/templates/base-project/packages/db/src/index.js +3 -0
- package/templates/base-project/packages/db/src/queries.js +267 -0
- package/templates/base-project/packages/db/src/queries.test.js +79 -0
- package/templates/base-project/packages/db/src/schemas.js +31 -0
- package/templates/base-project/pnpm-workspace.yaml +7 -0
- package/templates/base-project/turbo.json +25 -0
- package/templates/config/package.json +8 -0
- package/templates/config/src/index.js +21 -0
- package/templates/jobs/package.json +8 -0
- package/templates/jobs/src/definitions.js +9 -0
- package/templates/jobs/src/handlers.js +20 -0
- package/templates/jobs/src/index.js +2 -0
- package/templates/ui/package.json +11 -0
- package/templates/ui/src/button.js +19 -0
- package/templates/ui/src/card.js +14 -0
- package/templates/ui/src/index.js +3 -0
- 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,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 @@
|
|
|
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,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
|
+
}
|
|
Binary file
|
|
@@ -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,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);
|