@usequark/quark-create-app 1.23.11
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 +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -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/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- package/templates/worker/src/lib/tokens.js +81 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { validateBody, withCsrfProtection } from "@techstream/quark-core";
|
|
2
|
+
import { user, userUpdateSchema } from "@techstream/quark-db";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
import { requireRole } from "@/lib/auth-middleware";
|
|
5
|
+
import { handleError } from "../../error-handler";
|
|
6
|
+
|
|
7
|
+
export async function GET(_request, { params }) {
|
|
8
|
+
try {
|
|
9
|
+
await requireRole("admin");
|
|
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 const PATCH = withCsrfProtection(async (request, { params }) => {
|
|
22
|
+
try {
|
|
23
|
+
await requireRole("admin");
|
|
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 const DELETE = withCsrfProtection(async (_request, { params }) => {
|
|
40
|
+
try {
|
|
41
|
+
await requireRole("admin");
|
|
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,67 @@
|
|
|
1
|
+
import { validateBody, withCsrfProtection } from "@techstream/quark-core";
|
|
2
|
+
import { user, userUpdateSchema } from "@techstream/quark-db";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
import { extractBearerPayload } from "../../../../lib/jwt";
|
|
5
|
+
import { handleError } from "../../error-handler";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extract the authenticated user's ID from the Bearer token.
|
|
9
|
+
* Returns null and sends a 401 response if not authenticated.
|
|
10
|
+
*/
|
|
11
|
+
async function requireBearerUser(request) {
|
|
12
|
+
const payload = await extractBearerPayload(request);
|
|
13
|
+
if (!payload?.sub) {
|
|
14
|
+
return {
|
|
15
|
+
userId: null,
|
|
16
|
+
response: NextResponse.json(
|
|
17
|
+
{ message: "Authentication required" },
|
|
18
|
+
{ status: 401 },
|
|
19
|
+
),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { userId: payload.sub, response: null };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* GET /api/users/me
|
|
27
|
+
* Return the authenticated user's profile.
|
|
28
|
+
* Requires a valid Bearer JWT (mobile app auth).
|
|
29
|
+
*/
|
|
30
|
+
export async function GET(request) {
|
|
31
|
+
try {
|
|
32
|
+
const { userId, response } = await requireBearerUser(request);
|
|
33
|
+
if (response) return response;
|
|
34
|
+
|
|
35
|
+
const foundUser = await user.findById(userId);
|
|
36
|
+
if (!foundUser) {
|
|
37
|
+
return NextResponse.json({ message: "User not found" }, { status: 404 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return NextResponse.json(foundUser);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return handleError(error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* PATCH /api/users/me
|
|
48
|
+
* Update the authenticated user's profile.
|
|
49
|
+
* Requires a valid Bearer JWT (mobile app auth).
|
|
50
|
+
*/
|
|
51
|
+
export const PATCH = withCsrfProtection(async (request) => {
|
|
52
|
+
try {
|
|
53
|
+
const { userId, response } = await requireBearerUser(request);
|
|
54
|
+
if (response) return response;
|
|
55
|
+
|
|
56
|
+
const existingUser = await user.findById(userId);
|
|
57
|
+
if (!existingUser) {
|
|
58
|
+
return NextResponse.json({ message: "User not found" }, { status: 404 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const data = await validateBody(request, userUpdateSchema);
|
|
62
|
+
const updatedUser = await user.update(userId, data);
|
|
63
|
+
return NextResponse.json(updatedUser);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return handleError(error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parsePaginationQuery,
|
|
3
|
+
validateBody,
|
|
4
|
+
withCsrfProtection,
|
|
5
|
+
} from "@techstream/quark-core";
|
|
6
|
+
import { user, userCreateSchema } from "@techstream/quark-db";
|
|
7
|
+
import { NextResponse } from "next/server";
|
|
8
|
+
import { requireRole } from "@/lib/auth-middleware";
|
|
9
|
+
import { handleError } from "../error-handler";
|
|
10
|
+
|
|
11
|
+
export async function GET(request) {
|
|
12
|
+
try {
|
|
13
|
+
await requireRole("admin");
|
|
14
|
+
const { searchParams } = new URL(request.url);
|
|
15
|
+
const { skip, take, meta } = parsePaginationQuery(searchParams);
|
|
16
|
+
const [users, total] = await Promise.all([
|
|
17
|
+
user.findAll({ skip, take }),
|
|
18
|
+
user.count(),
|
|
19
|
+
]);
|
|
20
|
+
return NextResponse.json({ data: users, pagination: meta(total) });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return handleError(error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const POST = withCsrfProtection(async (request) => {
|
|
27
|
+
try {
|
|
28
|
+
await requireRole("admin");
|
|
29
|
+
const data = await validateBody(request, userCreateSchema);
|
|
30
|
+
|
|
31
|
+
// Check if email already exists
|
|
32
|
+
const existing = await user.findByEmail(data.email);
|
|
33
|
+
if (existing) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ message: "User with this email already exists" },
|
|
36
|
+
{ status: 409 },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const newUser = await user.create(data);
|
|
41
|
+
return NextResponse.json(newUser, { status: 201 });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return handleError(error);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { QuarkLogo } from "@techstream/quark-ui";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
title: "Auth Error",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const ERROR_MESSAGES = {
|
|
9
|
+
Configuration: "There is a problem with the server configuration.",
|
|
10
|
+
AccessDenied: "Access denied. You do not have permission to sign in.",
|
|
11
|
+
Verification: "The verification link has expired or has already been used.",
|
|
12
|
+
CredentialsSignin: "Invalid email or password.",
|
|
13
|
+
OAuthAccountNotLinked:
|
|
14
|
+
"This email is already associated with another sign-in method.",
|
|
15
|
+
OAuthSignin: "Could not start the sign-in flow.",
|
|
16
|
+
OAuthCallback: "Authentication callback failed.",
|
|
17
|
+
Default: "An unexpected error occurred.",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default async function AuthErrorPage({ searchParams }) {
|
|
21
|
+
const params = await searchParams;
|
|
22
|
+
const raw = params?.error;
|
|
23
|
+
// NextAuth sometimes passes the literal string "undefined" - normalise it.
|
|
24
|
+
const errorCode = !raw || raw === "undefined" ? "Default" : raw;
|
|
25
|
+
const message = ERROR_MESSAGES[errorCode] ?? ERROR_MESSAGES.Default;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="quark-auth-layout">
|
|
29
|
+
{/* Authority / branding panel */}
|
|
30
|
+
<div className="quark-auth-brand">
|
|
31
|
+
<Link href="/" aria-label="Go to home">
|
|
32
|
+
<QuarkLogo size={64} className="mb-8" />
|
|
33
|
+
</Link>
|
|
34
|
+
<p className="text-xs uppercase tracking-widest text-text-faint mb-3">
|
|
35
|
+
Authentication
|
|
36
|
+
</p>
|
|
37
|
+
<h2 className="text-3xl font-bold text-text leading-tight mb-4">
|
|
38
|
+
Something went wrong.
|
|
39
|
+
</h2>
|
|
40
|
+
<p className="text-sm text-text-muted leading-relaxed">
|
|
41
|
+
An error occurred during authentication.
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{/* Form panel */}
|
|
46
|
+
<div className="quark-auth-form">
|
|
47
|
+
{/* Mobile-only logo */}
|
|
48
|
+
<div className="flex md:hidden mb-8">
|
|
49
|
+
<Link href="/" aria-label="Go to home">
|
|
50
|
+
<QuarkLogo size={44} />
|
|
51
|
+
</Link>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="quark-auth-panel w-full max-w-sm">
|
|
55
|
+
<div className="mb-8">
|
|
56
|
+
<h1 className="text-2xl font-bold tracking-tight text-text">
|
|
57
|
+
Authentication error
|
|
58
|
+
</h1>
|
|
59
|
+
{errorCode !== "Default" && (
|
|
60
|
+
<p className="text-xs uppercase tracking-widest text-text-muted mt-2">
|
|
61
|
+
Error code: {errorCode}
|
|
62
|
+
</p>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div
|
|
67
|
+
role="alert"
|
|
68
|
+
className="border border-danger/40 bg-danger-muted px-4 py-3 text-sm text-danger mb-8"
|
|
69
|
+
>
|
|
70
|
+
{message}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<Link
|
|
74
|
+
href="/auth/signin"
|
|
75
|
+
className="text-xs text-text-muted hover:text-text transition-opacity duration-200 linear uppercase tracking-widest"
|
|
76
|
+
>
|
|
77
|
+
← Back to sign in
|
|
78
|
+
</Link>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { QuarkLogo } from "@techstream/quark-ui";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
title: "Forgot Password",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function ForgotPasswordPage() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="quark-auth-layout">
|
|
11
|
+
{/* Authority / branding panel */}
|
|
12
|
+
<div className="quark-auth-brand">
|
|
13
|
+
<Link href="/" aria-label="Go to home">
|
|
14
|
+
<QuarkLogo size={64} className="mb-8" />
|
|
15
|
+
</Link>
|
|
16
|
+
<p className="text-xs uppercase tracking-widest text-text-faint mb-3">
|
|
17
|
+
Account recovery
|
|
18
|
+
</p>
|
|
19
|
+
<h2 className="text-3xl font-bold text-text leading-tight mb-4">
|
|
20
|
+
Reset your password.
|
|
21
|
+
</h2>
|
|
22
|
+
<p className="text-sm text-text-muted leading-relaxed">
|
|
23
|
+
Enter your email and we will send you a link to recover access.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{/* Form panel */}
|
|
28
|
+
<div className="quark-auth-form">
|
|
29
|
+
{/* Mobile-only logo */}
|
|
30
|
+
<div className="flex md:hidden mb-8">
|
|
31
|
+
<Link href="/" aria-label="Go to home">
|
|
32
|
+
<QuarkLogo size={44} />
|
|
33
|
+
</Link>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="quark-auth-panel w-full max-w-sm">
|
|
37
|
+
<div className="mb-8">
|
|
38
|
+
<h1 className="text-2xl font-bold tracking-tight text-text">
|
|
39
|
+
Forgot password
|
|
40
|
+
</h1>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<p className="text-sm text-text-muted leading-relaxed mb-8">
|
|
44
|
+
Password reset is not available. Please contact support for help
|
|
45
|
+
accessing your account.
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
<Link
|
|
49
|
+
href="/auth/signin"
|
|
50
|
+
className="text-xs text-text-muted hover:text-text transition-opacity duration-200 linear uppercase tracking-widest"
|
|
51
|
+
>
|
|
52
|
+
← Back to sign in
|
|
53
|
+
</Link>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const AUTH_PROVIDER_LOAD_ERROR =
|
|
2
|
+
"Third-party sign-in is temporarily unavailable.";
|
|
3
|
+
|
|
4
|
+
export async function loadAuthProviders(fetchImpl = fetch) {
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetchImpl("/api/auth/providers");
|
|
7
|
+
|
|
8
|
+
if (!response.ok) {
|
|
9
|
+
return {
|
|
10
|
+
providers: null,
|
|
11
|
+
error: AUTH_PROVIDER_LOAD_ERROR,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
providers: await response.json(),
|
|
17
|
+
error: "",
|
|
18
|
+
};
|
|
19
|
+
} catch {
|
|
20
|
+
return {
|
|
21
|
+
providers: null,
|
|
22
|
+
error: AUTH_PROVIDER_LOAD_ERROR,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ErrorBanner,
|
|
6
|
+
Input,
|
|
7
|
+
Label,
|
|
8
|
+
PasswordInput,
|
|
9
|
+
QuarkLogo,
|
|
10
|
+
} from "@techstream/quark-ui";
|
|
11
|
+
import Link from "next/link";
|
|
12
|
+
import { useSearchParams } from "next/navigation";
|
|
13
|
+
import { signIn } from "next-auth/react";
|
|
14
|
+
import { Suspense, useEffect, useState } from "react";
|
|
15
|
+
import { loadAuthProviders } from "../provider-loading";
|
|
16
|
+
|
|
17
|
+
const STRENGTH_LABELS = ["", "Weak", "Fair", "Good", "Strong"];
|
|
18
|
+
const STRENGTH_COLORS = [
|
|
19
|
+
"",
|
|
20
|
+
"bg-danger",
|
|
21
|
+
"bg-warning",
|
|
22
|
+
"bg-primary",
|
|
23
|
+
"bg-success",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function getPasswordStrength(value) {
|
|
27
|
+
if (!value) return 0;
|
|
28
|
+
let score = 0;
|
|
29
|
+
if (value.length >= 8) score++;
|
|
30
|
+
if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score++;
|
|
31
|
+
if (/\d/.test(value)) score++;
|
|
32
|
+
if (/[^a-zA-Z0-9]/.test(value)) score++;
|
|
33
|
+
return score;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function RegisterPageClient() {
|
|
37
|
+
return (
|
|
38
|
+
<Suspense>
|
|
39
|
+
<RegisterForm />
|
|
40
|
+
</Suspense>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function RegisterForm() {
|
|
45
|
+
const searchParams = useSearchParams();
|
|
46
|
+
const callbackUrl = searchParams.get("callbackUrl") || "/";
|
|
47
|
+
|
|
48
|
+
const [name, setName] = useState("");
|
|
49
|
+
const [email, setEmail] = useState("");
|
|
50
|
+
const [password, setPassword] = useState("");
|
|
51
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
52
|
+
const [passwordError, setPasswordError] = useState("");
|
|
53
|
+
const [loading, setLoading] = useState(false);
|
|
54
|
+
const [providers, setProviders] = useState(null);
|
|
55
|
+
const [providerError, setProviderError] = useState("");
|
|
56
|
+
const [error, setError] = useState("");
|
|
57
|
+
const strength = getPasswordStrength(password);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
|
|
62
|
+
void loadAuthProviders().then(({ providers: loadedProviders, error }) => {
|
|
63
|
+
if (cancelled) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setProviders(loadedProviders);
|
|
68
|
+
setProviderError(error);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
function validatePassword(value) {
|
|
77
|
+
if (getPasswordStrength(value) < 3) {
|
|
78
|
+
return "Must be 8+ characters with uppercase, lowercase, and a number.";
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleSubmit(e) {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
setError("");
|
|
86
|
+
|
|
87
|
+
const pwdErr = validatePassword(password);
|
|
88
|
+
if (pwdErr) {
|
|
89
|
+
setPasswordError(pwdErr);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (password !== confirmPassword) {
|
|
94
|
+
setPasswordError("Passwords do not match.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setPasswordError("");
|
|
99
|
+
setLoading(true);
|
|
100
|
+
|
|
101
|
+
const res = await fetch("/api/auth/register", {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
email: email.trim(),
|
|
106
|
+
password,
|
|
107
|
+
name: name.trim() || undefined,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
const body = await res.json().catch(() => ({}));
|
|
113
|
+
setError(body.message || "Registration failed. Try again.");
|
|
114
|
+
setLoading(false);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = await signIn("credentials", {
|
|
119
|
+
email: email.trim(),
|
|
120
|
+
password,
|
|
121
|
+
callbackUrl,
|
|
122
|
+
redirect: false,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (result?.error) {
|
|
126
|
+
window.location.href = "/auth/signin";
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!result?.url) {
|
|
131
|
+
window.location.href = "/";
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
window.location.href = result.url;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function handleOAuth(provider) {
|
|
139
|
+
signIn(provider, { callbackUrl });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="quark-auth-layout">
|
|
144
|
+
<div className="quark-auth-brand">
|
|
145
|
+
<Link href="/" aria-label="Go to home">
|
|
146
|
+
<QuarkLogo size={64} className="mb-8" />
|
|
147
|
+
</Link>
|
|
148
|
+
<p className="text-xs uppercase tracking-widest text-text-faint mb-3">
|
|
149
|
+
Get started
|
|
150
|
+
</p>
|
|
151
|
+
<h2 className="text-3xl font-bold text-text leading-tight mb-4">
|
|
152
|
+
Everything you need,
|
|
153
|
+
<br />
|
|
154
|
+
ready in seconds.
|
|
155
|
+
</h2>
|
|
156
|
+
<p className="text-sm text-text-muted leading-relaxed">
|
|
157
|
+
No credit card required. No setup complexity.
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div className="quark-auth-form">
|
|
162
|
+
<div className="flex md:hidden mb-8">
|
|
163
|
+
<Link href="/" aria-label="Go to home">
|
|
164
|
+
<QuarkLogo size={44} />
|
|
165
|
+
</Link>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div className="quark-auth-panel w-full max-w-sm">
|
|
169
|
+
<div className="mb-8">
|
|
170
|
+
<h1 className="text-2xl font-bold tracking-tight text-text">
|
|
171
|
+
Create account
|
|
172
|
+
</h1>
|
|
173
|
+
<p className="text-xs uppercase tracking-widest text-text-muted mt-2">
|
|
174
|
+
Fill in the details below.
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<ErrorBanner message={error} />
|
|
179
|
+
|
|
180
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
181
|
+
<div className="space-y-2">
|
|
182
|
+
<Label htmlFor="name">
|
|
183
|
+
Name{" "}
|
|
184
|
+
<span className="text-text-faint font-normal normal-case tracking-normal">
|
|
185
|
+
(optional)
|
|
186
|
+
</span>
|
|
187
|
+
</Label>
|
|
188
|
+
<Input
|
|
189
|
+
id="name"
|
|
190
|
+
type="text"
|
|
191
|
+
autoComplete="name"
|
|
192
|
+
value={name}
|
|
193
|
+
onChange={(e) => setName(e.target.value)}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="space-y-2">
|
|
198
|
+
<Label htmlFor="email">Email</Label>
|
|
199
|
+
<Input
|
|
200
|
+
id="email"
|
|
201
|
+
type="email"
|
|
202
|
+
autoComplete="email"
|
|
203
|
+
required
|
|
204
|
+
value={email}
|
|
205
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div className="space-y-2">
|
|
210
|
+
<Label htmlFor="password">Password</Label>
|
|
211
|
+
<PasswordInput
|
|
212
|
+
id="password"
|
|
213
|
+
autoComplete="new-password"
|
|
214
|
+
required
|
|
215
|
+
value={password}
|
|
216
|
+
onChange={(e) => {
|
|
217
|
+
setPassword(e.target.value);
|
|
218
|
+
if (passwordError)
|
|
219
|
+
setPasswordError(validatePassword(e.target.value));
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
{password && (
|
|
223
|
+
<div className="space-y-1">
|
|
224
|
+
<div className="flex gap-0.5">
|
|
225
|
+
{[1, 2, 3, 4].map((seg) => (
|
|
226
|
+
<div
|
|
227
|
+
key={seg}
|
|
228
|
+
className={`h-0.5 flex-1 transition-colors duration-200 linear ${strength >= seg ? STRENGTH_COLORS[strength] : "bg-border"}`}
|
|
229
|
+
/>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
<p className="text-xs text-text-faint uppercase tracking-widest">
|
|
233
|
+
{STRENGTH_LABELS[strength]}
|
|
234
|
+
</p>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
<ul className="space-y-0.5">
|
|
238
|
+
{[
|
|
239
|
+
{
|
|
240
|
+
test: password.length >= 8,
|
|
241
|
+
label: "At least 8 characters",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
test: /[a-z]/.test(password) && /[A-Z]/.test(password),
|
|
245
|
+
label: "Uppercase & lowercase",
|
|
246
|
+
},
|
|
247
|
+
{ test: /\d/.test(password), label: "One number" },
|
|
248
|
+
].map(({ test, label }) => (
|
|
249
|
+
<li
|
|
250
|
+
key={label}
|
|
251
|
+
className={`text-xs uppercase tracking-widest ${test ? "text-success" : "text-text-faint"}`}
|
|
252
|
+
>
|
|
253
|
+
{test ? "✓" : "·"} {label}
|
|
254
|
+
</li>
|
|
255
|
+
))}
|
|
256
|
+
</ul>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div className="space-y-2">
|
|
260
|
+
<Label htmlFor="confirm-password">Confirm password</Label>
|
|
261
|
+
<PasswordInput
|
|
262
|
+
id="confirm-password"
|
|
263
|
+
autoComplete="new-password"
|
|
264
|
+
required
|
|
265
|
+
value={confirmPassword}
|
|
266
|
+
onChange={(e) => {
|
|
267
|
+
setConfirmPassword(e.target.value);
|
|
268
|
+
if (passwordError) setPasswordError("");
|
|
269
|
+
}}
|
|
270
|
+
/>
|
|
271
|
+
{passwordError ? (
|
|
272
|
+
<p className="text-xs text-danger uppercase tracking-widest">
|
|
273
|
+
{passwordError}
|
|
274
|
+
</p>
|
|
275
|
+
) : null}
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<Button
|
|
279
|
+
type="submit"
|
|
280
|
+
variant="primary"
|
|
281
|
+
className="w-full mt-2"
|
|
282
|
+
disabled={loading}
|
|
283
|
+
>
|
|
284
|
+
{loading ? "Creating account…" : "Get started →"}
|
|
285
|
+
</Button>
|
|
286
|
+
</form>
|
|
287
|
+
|
|
288
|
+
{providerError ? (
|
|
289
|
+
<p className="mt-4 text-xs uppercase tracking-widest text-text-muted">
|
|
290
|
+
{providerError}
|
|
291
|
+
</p>
|
|
292
|
+
) : null}
|
|
293
|
+
|
|
294
|
+
{providers?.github || providers?.google ? (
|
|
295
|
+
<>
|
|
296
|
+
<div className="mt-6 flex items-center gap-3">
|
|
297
|
+
<hr className="flex-1 border-border" />
|
|
298
|
+
<span className="text-xs text-text-muted uppercase tracking-widest">
|
|
299
|
+
or
|
|
300
|
+
</span>
|
|
301
|
+
<hr className="flex-1 border-border" />
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div className="mt-4 flex flex-col gap-2">
|
|
305
|
+
{providers.github && (
|
|
306
|
+
<Button
|
|
307
|
+
variant="secondary"
|
|
308
|
+
className="w-full"
|
|
309
|
+
onClick={() => handleOAuth("github")}
|
|
310
|
+
>
|
|
311
|
+
Continue with GitHub
|
|
312
|
+
</Button>
|
|
313
|
+
)}
|
|
314
|
+
{providers.google && (
|
|
315
|
+
<Button
|
|
316
|
+
variant="secondary"
|
|
317
|
+
className="w-full"
|
|
318
|
+
onClick={() => handleOAuth("google")}
|
|
319
|
+
>
|
|
320
|
+
Continue with Google
|
|
321
|
+
</Button>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
</>
|
|
325
|
+
) : null}
|
|
326
|
+
|
|
327
|
+
<p className="mt-8 text-xs text-text-muted uppercase tracking-widest">
|
|
328
|
+
Already have an account?{" "}
|
|
329
|
+
<Link
|
|
330
|
+
href="/auth/signin"
|
|
331
|
+
className="text-primary hover:opacity-75 transition-opacity duration-200 linear"
|
|
332
|
+
>
|
|
333
|
+
Sign in
|
|
334
|
+
</Link>
|
|
335
|
+
</p>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
|
|
3
|
+
import { isSignupEnabled } from "@/lib/auth-signup";
|
|
4
|
+
|
|
5
|
+
import RegisterPageClient from "./RegisterPageClient";
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
title: "Register",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function RegisterPage() {
|
|
12
|
+
if (!isSignupEnabled()) {
|
|
13
|
+
redirect("/auth/signin");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return <RegisterPageClient />;
|
|
17
|
+
}
|