@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.
Files changed (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. 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
+ }