@xk2800/nextjs-template 0.6.1-beta.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +56 -321
  2. package/components/dashboard/admin/adminUsersTable.tsx +36 -2
  3. package/components/dashboard/admin/feedbackTestForm.tsx +55 -0
  4. package/components/dashboard/admin/systemSettingsForm.tsx +28 -0
  5. package/components/dashboard/appSidebar.tsx +4 -4
  6. package/components/dashboard/deleteAccountCard.tsx +137 -0
  7. package/components/dashboard/errorFeedback.tsx +65 -0
  8. package/components/dashboard/feedbackDialog.tsx +160 -0
  9. package/components/dashboard/installAppCard.tsx +187 -0
  10. package/components/site/cookie-banner.tsx +57 -0
  11. package/components/ui/button.tsx +1 -1
  12. package/components/ui/file-upload.tsx +178 -0
  13. package/components/ui/input.tsx +1 -1
  14. package/components/ui/separator.tsx +1 -1
  15. package/components/ui/sheet.tsx +1 -1
  16. package/components/ui/sidebar.tsx +8 -8
  17. package/components/ui/skeleton.tsx +1 -1
  18. package/components/ui/tooltip.tsx +1 -1
  19. package/dist/activity/logger.js +21 -1
  20. package/dist/activity/logger.js.map +1 -1
  21. package/dist/activity/logger.mjs +4 -4
  22. package/dist/activity/queries.d.mts +1 -1
  23. package/dist/activity/queries.d.ts +1 -1
  24. package/dist/activity/queries.js +21 -1
  25. package/dist/activity/queries.js.map +1 -1
  26. package/dist/activity/queries.mjs +3 -3
  27. package/dist/admin/queries.js +21 -1
  28. package/dist/admin/queries.js.map +1 -1
  29. package/dist/admin/queries.mjs +3 -3
  30. package/dist/api/feedback.d.mts +23 -0
  31. package/dist/api/feedback.d.ts +23 -0
  32. package/dist/api/feedback.js +1224 -0
  33. package/dist/api/feedback.js.map +1 -0
  34. package/dist/api/feedback.mjs +136 -0
  35. package/dist/api/feedback.mjs.map +1 -0
  36. package/dist/auth/helpers.d.mts +3 -3
  37. package/dist/auth/helpers.d.ts +3 -3
  38. package/dist/auth/helpers.js +45 -4
  39. package/dist/auth/helpers.js.map +1 -1
  40. package/dist/auth/helpers.mjs +7 -7
  41. package/dist/auth/helpers.mjs.map +1 -1
  42. package/dist/auth/index.d.mts +8 -2
  43. package/dist/auth/index.d.ts +8 -2
  44. package/dist/auth/index.js +44 -3
  45. package/dist/auth/index.js.map +1 -1
  46. package/dist/auth/index.mjs +6 -6
  47. package/dist/auth-client/index.d.mts +16 -16
  48. package/dist/auth-client/index.d.ts +16 -16
  49. package/dist/{chunk-IF3JZRSZ.mjs → chunk-3MGEAGHU.mjs} +6 -5
  50. package/dist/chunk-3MGEAGHU.mjs.map +1 -0
  51. package/dist/chunk-KAEA35OC.mjs +76 -0
  52. package/dist/chunk-KAEA35OC.mjs.map +1 -0
  53. package/dist/{chunk-GTPTVNY2.mjs → chunk-TMJYAOC2.mjs} +10 -2
  54. package/dist/chunk-TMJYAOC2.mjs.map +1 -0
  55. package/dist/{chunk-OHAUKE2Q.mjs → chunk-VQ5B4MA7.mjs} +4 -4
  56. package/dist/{chunk-OHAUKE2Q.mjs.map → chunk-VQ5B4MA7.mjs.map} +1 -1
  57. package/dist/{chunk-5PYDAD4P.mjs → chunk-WZDAMCUX.mjs} +4 -4
  58. package/dist/{chunk-5PYDAD4P.mjs.map → chunk-WZDAMCUX.mjs.map} +1 -1
  59. package/dist/{chunk-V72CFFU2.mjs → chunk-XWVAJXAG.mjs} +31 -11
  60. package/dist/chunk-XWVAJXAG.mjs.map +1 -0
  61. package/dist/{chunk-4B7MJOLH.mjs → chunk-ZQCALTKB.mjs} +17 -5
  62. package/dist/chunk-ZQCALTKB.mjs.map +1 -0
  63. package/dist/config/env.d.mts +3 -0
  64. package/dist/config/env.d.ts +3 -0
  65. package/dist/config/env.js +8 -0
  66. package/dist/config/env.js.map +1 -1
  67. package/dist/config/env.mjs +1 -1
  68. package/dist/db/index.d.mts +1 -1
  69. package/dist/db/index.d.ts +1 -1
  70. package/dist/db/index.js +21 -1
  71. package/dist/db/index.js.map +1 -1
  72. package/dist/db/index.mjs +3 -3
  73. package/dist/db/schema.d.mts +1 -1
  74. package/dist/db/schema.d.ts +1 -1
  75. package/dist/db/schema.js +13 -1
  76. package/dist/db/schema.js.map +1 -1
  77. package/dist/db/schema.mjs +1 -1
  78. package/dist/errors.d.mts +29 -0
  79. package/dist/errors.d.ts +29 -0
  80. package/dist/errors.js +365 -0
  81. package/dist/errors.js.map +1 -0
  82. package/dist/errors.mjs +7 -0
  83. package/dist/errors.mjs.map +1 -0
  84. package/dist/{schema-BghJ8N4X.d.mts → schema-BNSBWNjK.d.mts} +99 -6
  85. package/dist/{schema-BghJ8N4X.d.ts → schema-BNSBWNjK.d.ts} +99 -6
  86. package/dist/sessions/queries.js +21 -1
  87. package/dist/sessions/queries.js.map +1 -1
  88. package/dist/sessions/queries.mjs +3 -3
  89. package/dist/settings/queries.d.mts +2 -2
  90. package/dist/settings/queries.d.ts +2 -2
  91. package/dist/settings/queries.js +22 -1
  92. package/dist/settings/queries.js.map +1 -1
  93. package/dist/settings/queries.mjs +4 -4
  94. package/dist/users/queries.d.mts +3 -3
  95. package/dist/users/queries.d.ts +3 -3
  96. package/dist/users/queries.js +21 -1
  97. package/dist/users/queries.js.map +1 -1
  98. package/dist/users/queries.mjs +3 -3
  99. package/hooks/use-mobile.ts +19 -0
  100. package/lib/device-fingerprint.ts +2 -3
  101. package/package.json +35 -32
  102. package/dist/chunk-4B7MJOLH.mjs.map +0 -1
  103. package/dist/chunk-GTPTVNY2.mjs.map +0 -1
  104. package/dist/chunk-IF3JZRSZ.mjs.map +0 -1
  105. package/dist/chunk-V72CFFU2.mjs.map +0 -1
package/README.md CHANGED
@@ -1,364 +1,99 @@
1
- This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
1
+ # Next.js Template
2
2
 
3
- This is a template for Next.js projects using Drizzle ORM, self hosted PostgreSQL for development and Neon DB for production.
3
+ A production-ready Next.js 16 starter: Better-Auth (email/password, Google, One Tap, passkeys, TOTP 2FA), Drizzle + PostgreSQL, an admin dashboard, security features and shadcn/ui. The reusable parts ship as the npm package [`@xk2800/nextjs-template`](#using-as-a-package), so projects get fixes with a version bump.
4
4
 
5
- ## Basic Folder Structure
5
+ **📚 Full documentation:** [`docs/content`](docs/content), or run it locally with `bun run docs:dev` (<http://localhost:3001>).
6
6
 
7
- ```plaintext {5}
8
- 📦 <project root>
9
- ├── 📂 server/
10
- │ ├── 📂 db/
11
- │ │ ├── 📜 schema.ts
12
- │ │ └── 📜 index.ts
13
- │ ├── 📂 drizzle/
14
- │ ├── 📜 auth.ts
15
- │ └── 📂 test-connection/
16
- │ └── 📜 index.ts
17
- ├── 📜 .env.production
18
- ├── 📜 .env.development
19
- ├── 📜 drizzle.config.ts
20
- ├── 📜 package.json
21
- └── 📜 tsconfig.json
22
- ```
23
-
24
- ## Getting Started
7
+ ## Quick start
25
8
 
26
- 1. Install the packages:
9
+ Requires [Bun](https://bun.sh), Git and a PostgreSQL database. Run this from the folder you want the project created in:
27
10
 
28
11
  ```bash
29
- bun install
12
+ npx @xk2800/create-nextjs
13
+ # or
14
+ bunx @xk2800/create-nextjs
30
15
  ```
31
16
 
32
- 2. Setup the `.env.development` and `.env.production` files based on `.env.development.example` and `.env.production.example` respectively.
33
-
34
- **Important:** Generate an `AUTH_SECRET` using:
35
-
36
- ```bash
37
- openssl rand -base64 32
38
- ```
39
-
40
- ## Migrating Schema
41
-
42
- 1. Generating schema, running this one command works for development and production modes `bun run generate`
43
-
44
- 2. Migrating schema into:
17
+ The CLI asks for a project name, a database driver (`pg` or `neon`) and optional modules (Doppler, Resend, Docker). It then creates `.env.development` with a generated secret, installs dependencies and makes the first commit. After that:
45
18
 
46
19
  ```bash
47
- # development
20
+ cd my-app
21
+ # set DATABASE_URL in .env.development
22
+ bun run doctor # checks env, auth secret, DB connection, pending migrations
48
23
  bun run migrate:dev
49
-
50
- # production
51
- bun run migrate:prod
52
- ```
53
-
54
- ## Secrets management with Doppler (optional)
55
-
56
- `.env.*` files work as described above by default. If you'd rather manage secrets in [Doppler](https://doppler.com) instead of local `.env` files, that's supported as an opt-in alternative — every `*:doppler` script does the same thing as its `.env`-based counterpart, just sourcing secrets from Doppler instead:
57
-
58
- ```bash
59
- bun run dev:doppler # instead of bun run dev
60
- bun run build:doppler # instead of bun run build
61
- bun run start:doppler # instead of bun run start
62
- bun run generate:doppler # instead of bun run generate
63
- bun run migrate:doppler # instead of bun run migrate:dev / migrate:prod / migrate:test
64
- bun run studio:doppler # instead of bun run studio:dev / studio:prod
65
- ```
66
-
67
- Setup (one-time, per machine):
68
-
69
- ```bash
70
- brew install dopplerhq/cli/doppler # or see https://docs.doppler.com/docs/install-cli
71
- doppler login
72
- doppler setup # run in the project root; select your Doppler project + config
24
+ bun dev
73
25
  ```
74
26
 
75
- `doppler setup` writes a `doppler.yaml` that maps this directory to a Doppler project/config — it holds no secrets itself, so it's safe to commit. `doppler run -- <command>` injects your Doppler secrets into `process.env` before the command starts; nothing in `config/env.ts` or `drizzle.config.ts` needed to change, since they just read `process.env` regardless of where it came from. Both Next.js and Bun treat already-set `process.env` values as higher priority than `.env` file values, so this layers safely even if `.env.development` is also present.
76
-
77
- To target a specific Doppler config without re-running `doppler setup` (e.g. in CI), pass `--config`: `doppler run --config prd -- bun run migrate:doppler`.
78
-
79
- ## Testing Connection
80
-
81
- 1. To test and ensure that the connection and database works, run:
82
-
83
- ```bash
84
- # development
85
- bun --env-file=.env.development server/test-connection/index.ts
27
+ Sign up at <http://localhost:3000>, then make yourself an admin:
86
28
 
87
- # production
88
- bun --env-file=.env.production server/test-connection/index.ts
29
+ ```sql
30
+ UPDATE "user" SET roles = 'admin' WHERE email = 'you@example.com';
89
31
  ```
90
32
 
91
33
  ## Features
92
34
 
93
- 1. Basic Next.js + Tailwind CSS boilerplate
94
- 2. Shadcn/ui
95
- 3. Better-Auth with Google OAuth + email/password
96
- 4. Basic login flow using Better-Auth
97
- 5. Connection template to a postgresql database for development, testing and production
98
- 6. Two-factor auth (TOTP + backup codes) and passkeys (WebAuthn), managed from the dashboard Settings page
99
- 7. Password reset and self-serve email verification, both via Resend-delivered React Email templates
100
- 8. Security email alerts — new-device sign-ins and passkey add/remove, mirrored to the activity log
101
- 9. Cookie-consent banner — "Necessary only" / "Accept all", with a "Cookie settings" link in the footer to change it later. The device fingerprint and Google One Tap only run after "Accept all"
102
-
103
- ## WIP
35
+ - **Auth:** email/password, Google OAuth, Google One Tap, passkeys, TOTP 2FA with backup codes, password reset, email verification
36
+ - **Security:** new-device email alerts, per-device sign-in/sign-up throttle, active-session revocation, self-serve account deletion, cookie-consent banner
37
+ - **Admin:** user management (roles, bans, bulk actions, CSV export), impersonation, system-wide activity log, device-security cards, live auth toggles, maintenance mode
38
+ - **Ops:** `bun run doctor`, Docker image, Doppler support, separate env files for dev, prod and test
104
39
 
105
- 1. Nothing in flight
40
+ ## Docs
106
41
 
107
- ## Using this as a package
42
+ | Topic | |
43
+ |---|---|
44
+ | Every env var, env vs. admin settings, Doppler | [Configuration](docs/content/configuration.mdx) |
45
+ | Folder layout, request flow, adding protected pages/routes | [Architecture](docs/content/architecture.mdx) |
46
+ | Tables, column gotchas, migrations | [Database](docs/content/database.mdx) |
47
+ | Providers, sessions, helpers, client usage | [Authentication](docs/content/authentication.mdx) |
48
+ | 2FA, passkeys, device alerts, throttle, cookie consent | [Security Features](docs/content/security.mdx) |
49
+ | Admin dashboard, impersonation, maintenance mode | [Admin & Maintenance](docs/content/admin.mdx) |
50
+ | Every route, body and guard | [API Reference](docs/content/api.mdx) |
51
+ | UI, auth, dashboard and email components | [Components](docs/content/components.mdx) |
52
+ | Installing the npm package | [Using as a Package](docs/content/npm-package.mdx) |
53
+ | Production checklist, migrations, Docker | [Deployment](docs/content/deployment.mdx) |
54
+ | Common errors and fixes | [Troubleshooting](docs/content/troubleshooting.mdx) |
108
55
 
109
- This repo also publishes its reusable auth/db/UI pieces as a public npm package, `@xk2800/nextjs-template`, to the npm registry. Use this in new projects instead of copy-pasting the whole template.
56
+ ## Using as a package
110
57
 
111
- ### 1. Install
58
+ For a **new** project, use the CLI above. It wires all of this up for you. To add the package to an **existing** app:
112
59
 
113
60
  ```bash
114
61
  bun add @xk2800/nextjs-template
115
62
  ```
116
63
 
117
- This has peer dependencies on `next`, `react`, `react-dom`, and the Radix/shadcn packages the UI components use (`@radix-ui/react-*`, `class-variance-authority`, `clsx`, `tailwind-merge`, `lucide-react`, `react-icons`). If your project already uses shadcn/ui, these are typically already installed. Three more peer deps are optional, only needed if you use the specific feature: `next-themes` (theme toggle/provider), `sonner` (dashboard session-revoke toasts), `@react-email/components` (email templates).
118
-
119
- ### 2. Enable `transpilePackages`
120
-
121
- UI components ship as raw `.tsx` source so `'use client'` boundaries survive. Add this to your `next.config.ts`:
122
-
123
64
  ```ts
124
- const nextConfig: NextConfig = {
125
- transpilePackages: ["@xk2800/nextjs-template"],
126
- };
65
+ // next.config.ts — components ship as raw .tsx
66
+ const nextConfig = { transpilePackages: ["@xk2800/nextjs-template"] }
127
67
  ```
128
68
 
129
- ### 3. Tailwind theme + content scanning
130
-
131
- The UI components use shadcn's CSS variable-based theme (`bg-primary`, `border-input`, etc.) and Tailwind v4 doesn't scan `node_modules` for class names by default. Add both to your `app/globals.css`, after the existing `@import "tailwindcss";`:
132
-
133
69
  ```css
70
+ /* app/globals.css */
134
71
  @import "tailwindcss";
135
72
  @import "@xk2800/nextjs-template/styles/theme.css";
136
73
  @source "../node_modules/@xk2800/nextjs-template";
137
74
  ```
138
75
 
139
- If you use `Dialog`, `AlertDialog`, or `DropdownMenu`, also install and import `tw-animate-css` for their open/close transitions — without it they still work, just without animation.
140
-
141
- ### 4. Adding a social provider (e.g. Apple) in one project only
142
-
143
- `auth` is the package's default instance (Google + email/password). To add another provider in a specific project — without forking the template — use the `createAuth` factory. This needs three changes, all in your own project, none in the package.
144
-
145
- **a. Create your own `server/auth.ts`** that wraps the factory:
146
-
147
- ```ts
148
- // server/auth.ts (your project, not the package)
149
- import { createAuth } from "@xk2800/nextjs-template/auth";
150
-
151
- export const auth = createAuth({
152
- socialProviders: {
153
- apple: {
154
- clientId: process.env.APPLE_CLIENT_ID!, // your Apple Services ID
155
- clientSecret: process.env.APPLE_CLIENT_SECRET!, // generated signed JWT, not a static secret
156
- appBundleIdentifier: process.env.APPLE_APP_BUNDLE_IDENTIFIER!, // only if you also support native/iOS sign-in
157
- },
158
- },
159
- });
160
- ```
161
-
162
- The `socialProviders` you pass are **merged** with the default `google` provider, not a replacement — `auth.socialProviders` ends up as `{ google, apple }`. Other config (session policy, password hashing, schema wiring) isn't exposed as an override; those stay fixed since they're shared assumptions, not per-project knobs.
163
-
164
- **b. Point your API route at your own `auth`, not the package's:**
165
-
166
76
  ```ts
167
77
  // app/api/auth/[...all]/route.ts
168
- import { auth } from "@/server/auth"; // your local file from step a — not "@xk2800/nextjs-template/auth"
169
- import { toNextJsHandler } from "better-auth/next-js";
170
-
171
- export const { GET, POST } = toNextJsHandler(auth);
172
- ```
173
-
174
- If you skip this and leave the route importing the package's `auth` directly, the server will never see your `apple` provider — the route handler is what actually serves the OAuth callback, so it has to be backed by the instance you customized.
175
-
176
- **c. Trigger it client-side.** The shipped `components/auth/socialLogin.tsx` is hardcoded to Google only (it's meant as a starting point, not a generic multi-provider switcher) — it won't grow an Apple button on its own. Add your own trigger using the same `authClient` the package already exports:
177
-
178
- ```tsx
179
- "use client";
180
- import { Button } from "@xk2800/nextjs-template/components/ui/button";
181
- import { authClient } from "@xk2800/nextjs-template/auth-client";
182
-
183
- export function AppleSignInButton({ callbackUrl }: { callbackUrl?: string }) {
184
- return (
185
- <Button variant="outline" className="w-full" onClick={() => authClient.signIn.social({ provider: "apple", callbackURL: callbackUrl || "/dashboard" })}>
186
- Sign in with Apple
187
- </Button>
188
- );
189
- }
190
- ```
191
-
192
- Nothing else needs to change — `proxy.ts`, `auth/helpers` (`requireAuth`, `hasRole`), and `auth-client`'s `useSession` are all provider-agnostic; they work off the session cookie/token, not which provider created it.
193
-
194
- #### Disabling Google or email/password instead
195
-
196
- Going the other way — turning **off** Google or email/password in one project — doesn't need `createAuth()` at all. Both providers are gated by env vars read from `config/env.ts`, which the package's `auth` singleton, the shipped `/login` and `/signup` pages, and any project that imports `@xk2800/nextjs-template/config/env` all read from the same place:
197
-
198
- ```bash
199
- # .env.* in your project (not the package) — either or both, default is true
200
- AUTH_ENABLE_GOOGLE=false
201
- AUTH_ENABLE_EMAIL_PASSWORD=false
78
+ import { auth } from "@xk2800/nextjs-template/auth"
79
+ import { toNextJsHandler } from "better-auth/next-js"
80
+ export const { GET, POST } = toNextJsHandler(auth)
202
81
  ```
203
82
 
204
- Setting one to `false` removes it from the Better-Auth config server-side (not just from the UI) — a disabled provider's endpoints won't authenticate anyone, even if called directly. The shipped login/signup pages hide the corresponding button/form automatically since they read the same flags.
205
-
206
- **Setting both `AUTH_ENABLE_GOOGLE=false` and `AUTH_ENABLE_EMAIL_PASSWORD=false` only fails if you haven't added a replacement provider.** `createAuth()` checks the *final* merged provider list — after your own `overrides.socialProviders` (per the section above) is applied — and throws only if that list is genuinely empty and email/password is also off. So:
207
-
208
- - Both flags `false`, no override → throws. There would be zero ways to sign in.
209
- - Both flags `false`, `createAuth({ socialProviders: { apple: {...} } })` → works fine. Apple is your only provider, and that's a valid config.
210
-
211
- This check lives in `createAuth()`, not `config/env.ts` — `config/env.ts` can't see overrides (they're only known at `createAuth()` call time), so it doesn't gate this at all. It only throws for the package's own default `auth` export (used by the shipped `/login`, `/signup`, and `app/api/auth/[...all]/route.ts`) if *that* ends up providerless, since that instance never has overrides.
83
+ Peer dependencies, the full import list, migrations, adding providers and One Tap are covered in **[Using as a Package](docs/content/npm-package.mdx)**.
212
84
 
213
- #### Enabling Google One Tap
214
-
215
- [Google One Tap](https://better-auth.com/docs/plugins/one-tap) is off by default and needs **two** flags, not one — `config/env.ts` is server-only (`import "server-only"`), so `lib/auth-client.ts` and the shipped `components/auth/oneTap.tsx` can't read it. The public flag is what actually gets inlined into your browser bundle; the server one only controls whether the Better-Auth server registers the plugin.
216
-
217
- ```bash
218
- # .env.* in your project — both default to false
219
- AUTH_ENABLE_ONE_TAP=true
220
- NEXT_PUBLIC_AUTH_ENABLE_ONE_TAP=true
85
+ ## Common scripts
221
86
 
222
- # needed either way — same OAuth client as AUTH_GOOGLE_ID, but public
223
- NEXT_PUBLIC_GOOGLE_CLIENT_ID=
224
- ```
225
-
226
- One Tap also requires Google to be enabled (`AUTH_ENABLE_GOOGLE=true`, the default) — the server silently skips registering the plugin if Google isn't configured, rather than throwing, since email/password-only projects are a valid config.
227
-
228
- Mount the shipped component once, anywhere it should be able to prompt (root layout is typical):
229
-
230
- ```tsx
231
- import OneTap from "@xk2800/nextjs-template/components/auth/oneTap";
232
- ```
233
-
234
- `<OneTap />` self-gates on `NEXT_PUBLIC_AUTH_ENABLE_ONE_TAP` and on the current session (`useSession`), so it's safe to leave mounted permanently — it no-ops for signed-in users and for any project that hasn't turned the flag on, no conditional JSX needed on your end. Toggling the feature later is just flipping both env vars, no code changes.
235
-
236
- Your Google Cloud OAuth client also needs its **Authorized JavaScript origins** to include whatever origin you're testing/deploying from (e.g. `http://localhost:3000`) — One Tap validates the origin client-side, separately from the OAuth redirect URIs you already set up.
237
-
238
- **One Tap waits for cookie consent.** It only prompts once the visitor clicks "Accept all" in the cookie banner (see below). If you import `<OneTap />` without mounting `<CookieBanner />`, set `NEXT_PUBLIC_COOKIE_BANNER=false`. Otherwise nobody can consent and the prompt never shows.
239
-
240
- #### Cookie-consent banner
241
-
242
- On by default. `<CookieBanner />` (mounted in `app/layout.tsx`) asks once and stores the answer in the first-party `cookie_consent` cookie (`all` | `necessary`, 1 year). `<CookieSettingsButton />` (in the site footer) clears the answer so the banner comes back. Auth/session cookies and UI preferences are "necessary" and always on. Optional, and only after "Accept all":
243
-
244
- - **FingerprintJS device id**: `getVisitorId()` in `lib/device-fingerprint.ts` returns `null` without consent. New-device alerts are then skipped, and the sign-in/sign-up throttle falls back to its per-IP cap.
245
- - **Google One Tap**: see above.
246
-
247
- ```tsx
248
- import { CookieBanner, CookieSettingsButton } from "@xk2800/nextjs-template/components/site/cookie-banner";
249
- ```
250
-
251
- Gate your own analytics or marketing scripts the same way:
252
-
253
- ```tsx
254
- import { useCookieConsent, hasOptionalConsent } from "@xk2800/nextjs-template/lib/cookie-consent";
255
-
256
- const consent = useCookieConsent(); // "all" | "necessary" | null (not chosen yet / on the server)
257
- if (consent === "all") { /* load analytics */ }
258
- ```
259
-
260
- Don't need a banner (no EU/UK users, nothing optional)? Set `NEXT_PUBLIC_COOKIE_BANNER=false`. The banner and the footer link disappear, and consent is treated as granted, so nothing gated on it breaks.
261
-
262
- ### 5. Import what you need
263
-
264
- ```ts
265
- // auth
266
- import { auth } from "@xk2800/nextjs-template/auth";
267
- import { requireAuth, hasRole } from "@xk2800/nextjs-template/auth/helpers";
268
- import { authClient, useSession } from "@xk2800/nextjs-template/auth-client";
269
-
270
- // db
271
- import { db } from "@xk2800/nextjs-template/db";
272
- import { users, sessions } from "@xk2800/nextjs-template/db/schema";
273
-
274
- // server-only query helpers (each throws if imported into a Client Component)
275
- import { getAdminStats } from "@xk2800/nextjs-template/admin/queries";
276
- import { getUserSessions } from "@xk2800/nextjs-template/sessions/queries";
277
- import { logActivity } from "@xk2800/nextjs-template/activity/logger";
278
- import { getUserActivityLogs, getAllActivityLogs } from "@xk2800/nextjs-template/activity/queries";
279
-
280
- // config, types, isomorphic helpers
281
- import { config } from "@xk2800/nextjs-template/config/env";
282
- import { LoginSchema } from "@xk2800/nextjs-template/types/auth/loginSchema";
283
- import { SignupSchema } from "@xk2800/nextjs-template/types/auth/signupSchema";
284
- import { formatDate, formatDateTime, getUserInitials } from "@xk2800/nextjs-template/lib/formatters";
285
-
286
- // ui primitives (raw source, any file under components/ui)
287
- import { Button } from "@xk2800/nextjs-template/components/ui/button";
288
-
289
- // auth UI
290
- import AuthCard from "@xk2800/nextjs-template/components/auth/authCard";
291
- import LogoutButtons from "@xk2800/nextjs-template/components/auth/logoutButtons";
292
- import EmailPasswordLogin from "@xk2800/nextjs-template/components/auth/emailPasswordLogin";
293
- import EmailPasswordSignup from "@xk2800/nextjs-template/components/auth/emailPasswordSignup";
294
- import OneTap from "@xk2800/nextjs-template/components/auth/oneTap"; // no-ops unless NEXT_PUBLIC_AUTH_ENABLE_ONE_TAP=true
295
-
296
- // dashboard UI (each takes data as props — fetch with the helpers above in your page, then pass down)
297
- import ProfileCard from "@xk2800/nextjs-template/components/dashboard/profileCard";
298
- import AccountDetailsCard from "@xk2800/nextjs-template/components/dashboard/accountDetailsCard";
299
- import SessionsCard from "@xk2800/nextjs-template/components/dashboard/sessionsCard";
300
- import AdminSection, { type AdminStats } from "@xk2800/nextjs-template/components/dashboard/adminSection";
301
- import ActivityLogsCard from "@xk2800/nextjs-template/components/dashboard/activityLogsCard";
302
- import DashboardHeader, { type DashboardNavLink } from "@xk2800/nextjs-template/components/dashboard/dashboardHeader"; // brandName, brandHref, navLinks are optional props
303
-
304
- // optional feature UI (needs the matching optional peer dep)
305
- import { ThemeProvider } from "@xk2800/nextjs-template/components/providers/theme-provider"; // next-themes
306
- import { ThemeToggle } from "@xk2800/nextjs-template/components/theme/theme-toggle"; // next-themes
307
- import { SendEmailButton } from "@xk2800/nextjs-template/components/email/send-email-button";
308
- import { EmailTemplate } from "@xk2800/nextjs-template/components/email/email-template"; // @react-email/components
309
- ```
310
-
311
- ### 6. Not importable — copy these patterns instead
312
-
313
- `app/api/auth/[...all]/route.ts` and `proxy.ts` are Next.js file-convention code, not library exports. Copy the pattern into your own project:
314
-
315
- ```ts
316
- // app/api/auth/[...all]/route.ts
317
- import { auth } from "@xk2800/nextjs-template/auth";
318
- import { toNextJsHandler } from "better-auth/next-js";
319
-
320
- export const { GET, POST } = toNextJsHandler(auth);
321
- ```
322
-
323
- ### 7. Database migrations
324
-
325
- The package does not ship migrations. drizzle-kit needs a local file it can import directly (it can't resolve package `exports` subpaths reliably), so add a one-line re-export in your own project:
326
-
327
- ```ts
328
- // server/db/schema.ts
329
- export * from "@xk2800/nextjs-template/db/schema";
330
- ```
331
-
332
- Then point `drizzle.config.ts` at it, reading `DATABASE_URL` from `process.env` directly rather than importing `@xk2800/nextjs-template/config/env` — that module is guarded with `server-only`, which throws when resolved outside Next.js's own bundler (drizzle-kit runs via plain Node/esbuild, so the guard fires unconditionally there):
333
-
334
- ```ts
335
- // drizzle.config.ts
336
- import { defineConfig } from "drizzle-kit";
337
-
338
- export default defineConfig({
339
- out: "./server/drizzle",
340
- schema: "./server/db/schema.ts",
341
- dialect: "postgresql",
342
- dbCredentials: { url: process.env.DATABASE_URL! },
343
- });
344
- ```
345
-
346
- ### 8. Secrets management with Doppler (optional)
347
-
348
- The package's `config/env.ts` and every server-only module just read `process.env.*` — they don't care whether values came from `.env` files or were injected by Doppler, so using Doppler in a consuming project needs zero changes to `@xk2800/nextjs-template` itself. Set it up the same way as in the template repo:
349
-
350
- ```bash
351
- doppler login
352
- doppler setup # in your project root, select/create a Doppler project + config
353
- ```
354
-
355
- Then wrap any command that needs your env vars with `doppler run --`, instead of relying on `.env.development`/`.env.local`:
356
-
357
- ```bash
358
- doppler run -- bun dev
359
- doppler run -- bun run generate # drizzle-kit generate
360
- doppler run -- bun run migrate # drizzle-kit migrate
361
- doppler run -- next build
362
- ```
87
+ | | |
88
+ |---|---|
89
+ | `bun dev` | Dev server |
90
+ | `bun run doctor` | Check env, auth secret, DB connection and pending migrations |
91
+ | `bun update @xk2800/nextjs-template` | Pull template updates (then `bun run generate`) |
92
+ | `bun run generate` | Generate a migration from `server/db/schema.ts` |
93
+ | `bun run migrate:dev` / `:prod` / `:test` | Apply migrations |
94
+ | `bun run studio:dev` | Drizzle Studio |
95
+ | `bun run test` | Unit tests |
96
+ | `bun run storybook` | Storybook for every component (mock data, no DB needed) |
97
+ | `bun run *:doppler` | Any of the above with secrets from Doppler |
363
98
 
364
- Since `doppler run` injects secrets into `process.env` before the command starts, and both Next.js and Bun treat already-set `process.env` values as higher priority than `.env` file values, you can even keep `.env.development` around as a local fallback without conflict — Doppler's values win when present.
99
+ **Working on the template itself:** clone this repo, copy `.env.development.example` to `.env.development`, then run `bun install`. Publishing steps are in [PUBLISHING.md](PUBLISHING.md).
@@ -69,7 +69,7 @@ export default function AdminUsersTable({ initialUsers, initialPagination }: Adm
69
69
  const [page, setPage] = useState(1)
70
70
  const [pagination, setPagination] = useState<PaginationData | null>(initialPagination)
71
71
  const [actionUserId, setActionUserId] = useState<string | null>(null)
72
- const [actionType, setActionType] = useState<'delete' | 'ban' | 'unban' | 'promote' | 'demote' | 'impersonate' | null>(null)
72
+ const [actionType, setActionType] = useState<'delete' | 'ban' | 'unban' | 'promote' | 'demote' | 'tester' | 'untester' | 'impersonate' | null>(null)
73
73
  const [isProcessing, setIsProcessing] = useState(false)
74
74
  const [isExporting, setIsExporting] = useState(false)
75
75
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
@@ -229,6 +229,22 @@ export default function AdminUsersTable({ initialUsers, initialPagination }: Adm
229
229
  })
230
230
  successMessage = 'Admin access removed'
231
231
  break
232
+ case 'tester':
233
+ response = await fetch(`/api/admin/users/${actionUserId}/role`, {
234
+ method: 'PATCH',
235
+ headers: { 'Content-Type': 'application/json' },
236
+ body: JSON.stringify({ role: 'tester' }),
237
+ })
238
+ successMessage = 'User marked as tester'
239
+ break
240
+ case 'untester':
241
+ response = await fetch(`/api/admin/users/${actionUserId}/role`, {
242
+ method: 'PATCH',
243
+ headers: { 'Content-Type': 'application/json' },
244
+ body: JSON.stringify({ role: 'user' }),
245
+ })
246
+ successMessage = 'Tester role removed'
247
+ break
232
248
  }
233
249
 
234
250
  if (!response?.ok) throw new Error('Action failed')
@@ -426,7 +442,7 @@ export default function AdminUsersTable({ initialUsers, initialPagination }: Adm
426
442
  title: 'Actions',
427
443
  minWidth: 340,
428
444
  renderer: (user) => (
429
- <div className="space-x-2 text-right">
445
+ <div className="flex flex-wrap justify-end gap-2">
430
446
  <Button
431
447
  variant="outline"
432
448
  size="sm"
@@ -462,6 +478,18 @@ export default function AdminUsersTable({ initialUsers, initialPagination }: Adm
462
478
  Make Admin
463
479
  </Button>
464
480
  )}
481
+ {user.role !== 'admin' && (
482
+ <Button
483
+ variant="outline"
484
+ size="sm"
485
+ onClick={() => {
486
+ setActionUserId(user.id)
487
+ setActionType(user.role === 'tester' ? 'untester' : 'tester')
488
+ }}
489
+ >
490
+ {user.role === 'tester' ? 'Remove Tester' : 'Make Tester'}
491
+ </Button>
492
+ )}
465
493
  {user.banned ? (
466
494
  <Button
467
495
  variant="outline"
@@ -530,6 +558,8 @@ export default function AdminUsersTable({ initialUsers, initialPagination }: Adm
530
558
  {actionType === 'unban' && 'Unban User?'}
531
559
  {actionType === 'promote' && 'Promote to Admin?'}
532
560
  {actionType === 'demote' && 'Remove Admin Access?'}
561
+ {actionType === 'tester' && 'Make Tester?'}
562
+ {actionType === 'untester' && 'Remove Tester Role?'}
533
563
  {actionType === 'impersonate' && 'Impersonate User?'}
534
564
  </AlertDialogTitle>
535
565
  <AlertDialogDescription>
@@ -540,6 +570,10 @@ export default function AdminUsersTable({ initialUsers, initialPagination }: Adm
540
570
  {actionType === 'unban' && 'This will restore the user access.'}
541
571
  {actionType === 'promote' &&
542
572
  'This will grant the user full admin dashboard access.'}
573
+ {actionType === 'tester' &&
574
+ 'Marks this account as a test account. It gets no admin access.'}
575
+ {actionType === 'untester' &&
576
+ 'Turns this test account back into a regular user.'}
543
577
  {actionType === 'demote' &&
544
578
  "This will revoke the user's admin dashboard access."}
545
579
  {actionType === 'impersonate' &&
@@ -0,0 +1,55 @@
1
+ 'use client'
2
+
3
+ import { useState } from "react"
4
+ import { toast } from "sonner"
5
+ import { Button } from "../../ui/button"
6
+ import { Input } from "../../ui/input"
7
+ import { Label } from "../../ui/label"
8
+ import { toastError } from "../errorFeedback"
9
+
10
+ /**
11
+ * Development-only: submits to /api/feedback/test so the error toast → Report
12
+ * → feedback flow can be tried end to end. "!error" fails with a safe message;
13
+ * "!crash" fails like a real bug (generic message + error ref, details in the
14
+ * server log only).
15
+ */
16
+ export default function FeedbackTestForm() {
17
+ const [text, setText] = useState("!crash")
18
+ const [isSending, setIsSending] = useState(false)
19
+
20
+ const handleSubmit = async (e: React.FormEvent) => {
21
+ e.preventDefault()
22
+ setIsSending(true)
23
+ try {
24
+ const response = await fetch("/api/feedback/test", {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify({ text }),
28
+ })
29
+ const data = await response.json().catch(() => ({}))
30
+ if (!response.ok) throw new Error(data.error || "Test request failed")
31
+ toast.success('No error. Submit "!crash" or "!error" to trigger one.')
32
+ } catch (error) {
33
+ toastError(error instanceof Error ? error.message : "Test request failed")
34
+ } finally {
35
+ setIsSending(false)
36
+ }
37
+ }
38
+
39
+ return (
40
+ <form onSubmit={handleSubmit} className="space-y-2 rounded-md border border-dashed p-3">
41
+ <Label htmlFor="feedback-test-input">Test error reporting (development only)</Label>
42
+ <p className="text-sm text-gray-500">
43
+ Submit &quot;!crash&quot; for an unexpected error (you get a ref, the details go to the
44
+ server log) or &quot;!error&quot; for a safe validation-style error. Then tap Report on
45
+ the toast.
46
+ </p>
47
+ <div className="flex gap-2">
48
+ <Input id="feedback-test-input" value={text} onChange={(e) => setText(e.target.value)} />
49
+ <Button type="submit" variant="outline" disabled={isSending}>
50
+ {isSending ? "Sending..." : "Submit"}
51
+ </Button>
52
+ </div>
53
+ </form>
54
+ )
55
+ }
@@ -9,6 +9,7 @@ import { Label } from "../../ui/label"
9
9
  import { Textarea } from "../../ui/textarea"
10
10
  import { Button } from "../../ui/button"
11
11
  import type { SystemSettings } from "@xk2800/nextjs-template/settings/queries"
12
+ import FeedbackTestForm from "./feedbackTestForm"
12
13
  import { ALWAYS_OPEN_PATHS, DEFAULT_MAINTENANCE_EXEMPT_PATHS } from "../../../lib/maintenance"
13
14
 
14
15
  interface ConfiguredAuthFlags {
@@ -50,6 +51,7 @@ export default function SystemSettingsForm({ initialSettings, configuredFlags }:
50
51
  authEnableEmailPassword: settings.authEnableEmailPassword,
51
52
  authEnableOneTap: settings.authEnableOneTap,
52
53
  enableSessionRevocation: settings.enableSessionRevocation,
54
+ feedbackAttachErrors: settings.feedbackAttachErrors,
53
55
  }),
54
56
  })
55
57
 
@@ -159,6 +161,32 @@ export default function SystemSettingsForm({ initialSettings, configuredFlags }:
159
161
  </CardContent>
160
162
  </Card>
161
163
 
164
+ <Card>
165
+ <CardHeader>
166
+ <CardTitle>Feedback</CardTitle>
167
+ <CardDescription>
168
+ Applies when FEEDBACK_DISCORD_WEBHOOK_URL or FEEDBACK_EMAIL is set
169
+ </CardDescription>
170
+ </CardHeader>
171
+ <CardContent className="space-y-4">
172
+ <div className="flex items-center justify-between gap-4">
173
+ <div>
174
+ <Label htmlFor="feedback-errors-toggle">Attach error details</Label>
175
+ <p className="text-sm text-gray-500">
176
+ When a user reports a problem from an error, send the error message with their
177
+ report, even if they edit it out of the text
178
+ </p>
179
+ </div>
180
+ <Switch
181
+ id="feedback-errors-toggle"
182
+ checked={settings.feedbackAttachErrors}
183
+ onCheckedChange={(checked) => setSettings({ ...settings, feedbackAttachErrors: checked })}
184
+ />
185
+ </div>
186
+ {process.env.NODE_ENV === "development" && <FeedbackTestForm />}
187
+ </CardContent>
188
+ </Card>
189
+
162
190
  <Card>
163
191
  <CardHeader>
164
192
  <CardTitle>Maintenance Mode</CardTitle>
@@ -13,7 +13,7 @@ import {
13
13
  LogOut,
14
14
  } from "lucide-react"
15
15
 
16
- import { authClient } from "@/lib/auth-client"
16
+ import { authClient } from "../../lib/auth-client"
17
17
  import {
18
18
  Sidebar,
19
19
  SidebarContent,
@@ -25,9 +25,9 @@ import {
25
25
  SidebarMenu,
26
26
  SidebarMenuButton,
27
27
  SidebarMenuItem,
28
- } from "@/components/ui/sidebar"
29
- import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
30
- import { Button } from "@/components/ui/button"
28
+ } from "../ui/sidebar"
29
+ import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"
30
+ import { Button } from "../ui/button"
31
31
 
32
32
  export interface AppSidebarUser {
33
33
  name: string