@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.
- package/README.md +56 -321
- package/components/dashboard/admin/adminUsersTable.tsx +36 -2
- package/components/dashboard/admin/feedbackTestForm.tsx +55 -0
- package/components/dashboard/admin/systemSettingsForm.tsx +28 -0
- package/components/dashboard/appSidebar.tsx +4 -4
- package/components/dashboard/deleteAccountCard.tsx +137 -0
- package/components/dashboard/errorFeedback.tsx +65 -0
- package/components/dashboard/feedbackDialog.tsx +160 -0
- package/components/dashboard/installAppCard.tsx +187 -0
- package/components/site/cookie-banner.tsx +57 -0
- package/components/ui/button.tsx +1 -1
- package/components/ui/file-upload.tsx +178 -0
- package/components/ui/input.tsx +1 -1
- package/components/ui/separator.tsx +1 -1
- package/components/ui/sheet.tsx +1 -1
- package/components/ui/sidebar.tsx +8 -8
- package/components/ui/skeleton.tsx +1 -1
- package/components/ui/tooltip.tsx +1 -1
- package/dist/activity/logger.js +21 -1
- package/dist/activity/logger.js.map +1 -1
- package/dist/activity/logger.mjs +4 -4
- package/dist/activity/queries.d.mts +1 -1
- package/dist/activity/queries.d.ts +1 -1
- package/dist/activity/queries.js +21 -1
- package/dist/activity/queries.js.map +1 -1
- package/dist/activity/queries.mjs +3 -3
- package/dist/admin/queries.js +21 -1
- package/dist/admin/queries.js.map +1 -1
- package/dist/admin/queries.mjs +3 -3
- package/dist/api/feedback.d.mts +23 -0
- package/dist/api/feedback.d.ts +23 -0
- package/dist/api/feedback.js +1224 -0
- package/dist/api/feedback.js.map +1 -0
- package/dist/api/feedback.mjs +136 -0
- package/dist/api/feedback.mjs.map +1 -0
- package/dist/auth/helpers.d.mts +3 -3
- package/dist/auth/helpers.d.ts +3 -3
- package/dist/auth/helpers.js +45 -4
- package/dist/auth/helpers.js.map +1 -1
- package/dist/auth/helpers.mjs +7 -7
- package/dist/auth/helpers.mjs.map +1 -1
- package/dist/auth/index.d.mts +8 -2
- package/dist/auth/index.d.ts +8 -2
- package/dist/auth/index.js +44 -3
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +6 -6
- package/dist/auth-client/index.d.mts +16 -16
- package/dist/auth-client/index.d.ts +16 -16
- package/dist/{chunk-IF3JZRSZ.mjs → chunk-3MGEAGHU.mjs} +6 -5
- package/dist/chunk-3MGEAGHU.mjs.map +1 -0
- package/dist/chunk-KAEA35OC.mjs +76 -0
- package/dist/chunk-KAEA35OC.mjs.map +1 -0
- package/dist/{chunk-GTPTVNY2.mjs → chunk-TMJYAOC2.mjs} +10 -2
- package/dist/chunk-TMJYAOC2.mjs.map +1 -0
- package/dist/{chunk-OHAUKE2Q.mjs → chunk-VQ5B4MA7.mjs} +4 -4
- package/dist/{chunk-OHAUKE2Q.mjs.map → chunk-VQ5B4MA7.mjs.map} +1 -1
- package/dist/{chunk-5PYDAD4P.mjs → chunk-WZDAMCUX.mjs} +4 -4
- package/dist/{chunk-5PYDAD4P.mjs.map → chunk-WZDAMCUX.mjs.map} +1 -1
- package/dist/{chunk-V72CFFU2.mjs → chunk-XWVAJXAG.mjs} +31 -11
- package/dist/chunk-XWVAJXAG.mjs.map +1 -0
- package/dist/{chunk-4B7MJOLH.mjs → chunk-ZQCALTKB.mjs} +17 -5
- package/dist/chunk-ZQCALTKB.mjs.map +1 -0
- package/dist/config/env.d.mts +3 -0
- package/dist/config/env.d.ts +3 -0
- package/dist/config/env.js +8 -0
- package/dist/config/env.js.map +1 -1
- package/dist/config/env.mjs +1 -1
- package/dist/db/index.d.mts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +21 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/index.mjs +3 -3
- package/dist/db/schema.d.mts +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +13 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/db/schema.mjs +1 -1
- package/dist/errors.d.mts +29 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +365 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +7 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/{schema-BghJ8N4X.d.mts → schema-BNSBWNjK.d.mts} +99 -6
- package/dist/{schema-BghJ8N4X.d.ts → schema-BNSBWNjK.d.ts} +99 -6
- package/dist/sessions/queries.js +21 -1
- package/dist/sessions/queries.js.map +1 -1
- package/dist/sessions/queries.mjs +3 -3
- package/dist/settings/queries.d.mts +2 -2
- package/dist/settings/queries.d.ts +2 -2
- package/dist/settings/queries.js +22 -1
- package/dist/settings/queries.js.map +1 -1
- package/dist/settings/queries.mjs +4 -4
- package/dist/users/queries.d.mts +3 -3
- package/dist/users/queries.d.ts +3 -3
- package/dist/users/queries.js +21 -1
- package/dist/users/queries.js.map +1 -1
- package/dist/users/queries.mjs +3 -3
- package/hooks/use-mobile.ts +19 -0
- package/lib/device-fingerprint.ts +2 -3
- package/package.json +35 -32
- package/dist/chunk-4B7MJOLH.mjs.map +0 -1
- package/dist/chunk-GTPTVNY2.mjs.map +0 -1
- package/dist/chunk-IF3JZRSZ.mjs.map +0 -1
- package/dist/chunk-V72CFFU2.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,364 +1,99 @@
|
|
|
1
|
-
|
|
1
|
+
# Next.js Template
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
**📚 Full documentation:** [`docs/content`](docs/content), or run it locally with `bun run docs:dev` (<http://localhost:3001>).
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
+
npx @xk2800/create-nextjs
|
|
13
|
+
# or
|
|
14
|
+
bunx @xk2800/create-nextjs
|
|
30
15
|
```
|
|
31
16
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
29
|
+
```sql
|
|
30
|
+
UPDATE "user" SET roles = 'admin' WHERE email = 'you@example.com';
|
|
89
31
|
```
|
|
90
32
|
|
|
91
33
|
## Features
|
|
92
34
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
40
|
+
## Docs
|
|
106
41
|
|
|
107
|
-
|
|
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
|
-
|
|
56
|
+
## Using as a package
|
|
110
57
|
|
|
111
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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="
|
|
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 "!crash" for an unexpected error (you get a ref, the details go to the
|
|
44
|
+
server log) or "!error" 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 "
|
|
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 "
|
|
29
|
-
import { Avatar, AvatarFallback, AvatarImage } from "
|
|
30
|
-
import { Button } from "
|
|
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
|