@xk2800/nextjs-template 0.5.3

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 (199) hide show
  1. package/README.md +339 -0
  2. package/components/auth/authCard.tsx +92 -0
  3. package/components/auth/authShell.tsx +46 -0
  4. package/components/auth/deviceCheck.tsx +52 -0
  5. package/components/auth/emailPasswordLogin.tsx +136 -0
  6. package/components/auth/emailPasswordSignup.tsx +178 -0
  7. package/components/auth/forgotPasswordForm.tsx +66 -0
  8. package/components/auth/loginErrorRecovery.tsx +117 -0
  9. package/components/auth/logoutButtons.tsx +26 -0
  10. package/components/auth/oneTap.tsx +50 -0
  11. package/components/auth/passkeyLogin.tsx +50 -0
  12. package/components/auth/resetPasswordForm.tsx +112 -0
  13. package/components/auth/socialLogin.tsx +51 -0
  14. package/components/auth/twoFactorVerifyForm.tsx +100 -0
  15. package/components/dashboard/accountDetailsCard.tsx +58 -0
  16. package/components/dashboard/activityLogsCard.tsx +141 -0
  17. package/components/dashboard/activityLogsCardSkeleton.tsx +20 -0
  18. package/components/dashboard/activityLogsSection.tsx +16 -0
  19. package/components/dashboard/admin/activityLogsSection.tsx +13 -0
  20. package/components/dashboard/admin/adminActivityLogsSkeleton.tsx +34 -0
  21. package/components/dashboard/admin/adminActivityLogsTable.tsx +319 -0
  22. package/components/dashboard/admin/adminStatsGrid.tsx +30 -0
  23. package/components/dashboard/admin/adminStatsGridSkeleton.tsx +16 -0
  24. package/components/dashboard/admin/adminUserActivityLogCard.tsx +53 -0
  25. package/components/dashboard/admin/adminUserActivityLogTable.tsx +69 -0
  26. package/components/dashboard/admin/adminUserLoginInfoCard.tsx +50 -0
  27. package/components/dashboard/admin/adminUserLoginInfoTable.tsx +63 -0
  28. package/components/dashboard/admin/adminUserSessionsCard.tsx +225 -0
  29. package/components/dashboard/admin/adminUserStatusCard.tsx +57 -0
  30. package/components/dashboard/admin/adminUsersSkeleton.tsx +30 -0
  31. package/components/dashboard/admin/adminUsersTable.tsx +596 -0
  32. package/components/dashboard/admin/deviceThrottleCard.tsx +59 -0
  33. package/components/dashboard/admin/deviceThrottleTable.tsx +93 -0
  34. package/components/dashboard/admin/impersonationLogCard.tsx +73 -0
  35. package/components/dashboard/admin/newDeviceCard.tsx +35 -0
  36. package/components/dashboard/admin/newDeviceTable.tsx +71 -0
  37. package/components/dashboard/admin/revokeAllSessionsButton.tsx +77 -0
  38. package/components/dashboard/admin/sharedDeviceCard.tsx +42 -0
  39. package/components/dashboard/admin/sharedDeviceTable.tsx +75 -0
  40. package/components/dashboard/admin/systemSettingsForm.tsx +200 -0
  41. package/components/dashboard/admin/systemSettingsSection.tsx +9 -0
  42. package/components/dashboard/admin/systemSettingsSkeleton.tsx +29 -0
  43. package/components/dashboard/admin/usersSection.tsx +13 -0
  44. package/components/dashboard/adminSection.tsx +31 -0
  45. package/components/dashboard/appSidebar.tsx +170 -0
  46. package/components/dashboard/dashboardHeader.tsx +85 -0
  47. package/components/dashboard/heartbeat.tsx +22 -0
  48. package/components/dashboard/impersonationBanner.tsx +50 -0
  49. package/components/dashboard/passkeysCard.tsx +187 -0
  50. package/components/dashboard/profileCard.tsx +42 -0
  51. package/components/dashboard/sectionErrorFallback.tsx +18 -0
  52. package/components/dashboard/sessionsCard.tsx +194 -0
  53. package/components/dashboard/sessionsCardSkeleton.tsx +20 -0
  54. package/components/dashboard/sessionsSection.tsx +22 -0
  55. package/components/dashboard/twoFactorCard.tsx +344 -0
  56. package/components/dashboard/verifyEmailButton.tsx +32 -0
  57. package/components/email/email-template-2.tsx +44 -0
  58. package/components/email/email-template-new-device.tsx +80 -0
  59. package/components/email/email-template-passkey-change.tsx +81 -0
  60. package/components/email/email-template-reset-password.tsx +56 -0
  61. package/components/email/email-template-verify-email.tsx +55 -0
  62. package/components/email/email-template-welcome.tsx +53 -0
  63. package/components/email/email-template.tsx +33 -0
  64. package/components/email/send-email-button.tsx +61 -0
  65. package/components/providers/theme-provider.tsx +9 -0
  66. package/components/theme/theme-toggle.tsx +53 -0
  67. package/components/ui/alert-dialog.tsx +141 -0
  68. package/components/ui/avatar.tsx +53 -0
  69. package/components/ui/badge.tsx +46 -0
  70. package/components/ui/button.tsx +64 -0
  71. package/components/ui/card.tsx +92 -0
  72. package/components/ui/checkbox.tsx +32 -0
  73. package/components/ui/data-table.tsx +302 -0
  74. package/components/ui/dialog.tsx +143 -0
  75. package/components/ui/dropdown-menu.tsx +203 -0
  76. package/components/ui/input.tsx +21 -0
  77. package/components/ui/label.tsx +26 -0
  78. package/components/ui/password-input.tsx +38 -0
  79. package/components/ui/separator.tsx +28 -0
  80. package/components/ui/sheet.tsx +143 -0
  81. package/components/ui/sidebar.tsx +726 -0
  82. package/components/ui/skeleton.tsx +13 -0
  83. package/components/ui/switch.tsx +35 -0
  84. package/components/ui/table.tsx +116 -0
  85. package/components/ui/textarea.tsx +18 -0
  86. package/components/ui/tooltip.tsx +57 -0
  87. package/dist/activity/logger.d.mts +18 -0
  88. package/dist/activity/logger.d.ts +18 -0
  89. package/dist/activity/logger.js +289 -0
  90. package/dist/activity/logger.js.map +1 -0
  91. package/dist/activity/logger.mjs +7 -0
  92. package/dist/activity/logger.mjs.map +1 -0
  93. package/dist/activity/queries.d.mts +98 -0
  94. package/dist/activity/queries.d.ts +98 -0
  95. package/dist/activity/queries.js +372 -0
  96. package/dist/activity/queries.js.map +1 -0
  97. package/dist/activity/queries.mjs +124 -0
  98. package/dist/activity/queries.mjs.map +1 -0
  99. package/dist/admin/queries.d.mts +68 -0
  100. package/dist/admin/queries.d.ts +68 -0
  101. package/dist/admin/queries.js +450 -0
  102. package/dist/admin/queries.js.map +1 -0
  103. package/dist/admin/queries.mjs +143 -0
  104. package/dist/admin/queries.mjs.map +1 -0
  105. package/dist/auth/helpers.d.mts +111 -0
  106. package/dist/auth/helpers.d.ts +111 -0
  107. package/dist/auth/helpers.js +1023 -0
  108. package/dist/auth/helpers.js.map +1 -0
  109. package/dist/auth/helpers.mjs +67 -0
  110. package/dist/auth/helpers.mjs.map +1 -0
  111. package/dist/auth/index.d.mts +6589 -0
  112. package/dist/auth/index.d.ts +6589 -0
  113. package/dist/auth/index.js +968 -0
  114. package/dist/auth/index.js.map +1 -0
  115. package/dist/auth/index.mjs +10 -0
  116. package/dist/auth/index.mjs.map +1 -0
  117. package/dist/auth-client/index.d.mts +4864 -0
  118. package/dist/auth-client/index.d.ts +4864 -0
  119. package/dist/auth-client/index.js +55 -0
  120. package/dist/auth-client/index.js.map +1 -0
  121. package/dist/auth-client/index.mjs +49 -0
  122. package/dist/auth-client/index.mjs.map +1 -0
  123. package/dist/chunk-2FW7C3KS.mjs +556 -0
  124. package/dist/chunk-2FW7C3KS.mjs.map +1 -0
  125. package/dist/chunk-ENGYO4K7.mjs +189 -0
  126. package/dist/chunk-ENGYO4K7.mjs.map +1 -0
  127. package/dist/chunk-GTPTVNY2.mjs +40 -0
  128. package/dist/chunk-GTPTVNY2.mjs.map +1 -0
  129. package/dist/chunk-HYKUB3A2.mjs +30 -0
  130. package/dist/chunk-HYKUB3A2.mjs.map +1 -0
  131. package/dist/chunk-JIYHBWAU.mjs +79 -0
  132. package/dist/chunk-JIYHBWAU.mjs.map +1 -0
  133. package/dist/chunk-K6OJCSJS.mjs +41 -0
  134. package/dist/chunk-K6OJCSJS.mjs.map +1 -0
  135. package/dist/chunk-N6ECRCHY.mjs +61 -0
  136. package/dist/chunk-N6ECRCHY.mjs.map +1 -0
  137. package/dist/chunk-YN4436PK.mjs +33 -0
  138. package/dist/chunk-YN4436PK.mjs.map +1 -0
  139. package/dist/config/env.d.mts +15 -0
  140. package/dist/config/env.d.ts +15 -0
  141. package/dist/config/env.js +42 -0
  142. package/dist/config/env.js.map +1 -0
  143. package/dist/config/env.mjs +4 -0
  144. package/dist/config/env.mjs.map +1 -0
  145. package/dist/db/index.d.mts +10 -0
  146. package/dist/db/index.d.ts +10 -0
  147. package/dist/db/index.js +252 -0
  148. package/dist/db/index.js.map +1 -0
  149. package/dist/db/index.mjs +6 -0
  150. package/dist/db/index.mjs.map +1 -0
  151. package/dist/db/schema.d.mts +2 -0
  152. package/dist/db/schema.d.ts +2 -0
  153. package/dist/db/schema.js +186 -0
  154. package/dist/db/schema.js.map +1 -0
  155. package/dist/db/schema.mjs +4 -0
  156. package/dist/db/schema.mjs.map +1 -0
  157. package/dist/schema-CgkDv4wJ.d.mts +1758 -0
  158. package/dist/schema-CgkDv4wJ.d.ts +1758 -0
  159. package/dist/sessions/queries.d.mts +34 -0
  160. package/dist/sessions/queries.d.ts +34 -0
  161. package/dist/sessions/queries.js +268 -0
  162. package/dist/sessions/queries.js.map +1 -0
  163. package/dist/sessions/queries.mjs +23 -0
  164. package/dist/sessions/queries.mjs.map +1 -0
  165. package/dist/settings/queries.d.mts +20 -0
  166. package/dist/settings/queries.d.ts +20 -0
  167. package/dist/settings/queries.js +344 -0
  168. package/dist/settings/queries.js.map +1 -0
  169. package/dist/settings/queries.mjs +7 -0
  170. package/dist/settings/queries.mjs.map +1 -0
  171. package/dist/types/auth/loginSchema.d.mts +8 -0
  172. package/dist/types/auth/loginSchema.d.ts +8 -0
  173. package/dist/types/auth/loginSchema.js +18 -0
  174. package/dist/types/auth/loginSchema.js.map +1 -0
  175. package/dist/types/auth/loginSchema.mjs +16 -0
  176. package/dist/types/auth/loginSchema.mjs.map +1 -0
  177. package/dist/types/auth/signupSchema.d.mts +9 -0
  178. package/dist/types/auth/signupSchema.d.ts +9 -0
  179. package/dist/types/auth/signupSchema.js +21 -0
  180. package/dist/types/auth/signupSchema.js.map +1 -0
  181. package/dist/types/auth/signupSchema.mjs +19 -0
  182. package/dist/types/auth/signupSchema.mjs.map +1 -0
  183. package/dist/users/queries.d.mts +71 -0
  184. package/dist/users/queries.d.ts +71 -0
  185. package/dist/users/queries.js +293 -0
  186. package/dist/users/queries.js.map +1 -0
  187. package/dist/users/queries.mjs +47 -0
  188. package/dist/users/queries.mjs.map +1 -0
  189. package/lib/auth-client.ts +58 -0
  190. package/lib/cookie-names.ts +13 -0
  191. package/lib/device-fingerprint.ts +21 -0
  192. package/lib/formatters.ts +53 -0
  193. package/lib/recalculate-columns.ts +75 -0
  194. package/lib/request-info.ts +78 -0
  195. package/lib/utils.ts +6 -0
  196. package/package.json +301 -0
  197. package/styles/theme.css +124 -0
  198. package/types/auth/loginSchema.ts +11 -0
  199. package/types/auth/signupSchema.ts +14 -0
package/README.md ADDED
@@ -0,0 +1,339 @@
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).
2
+
3
+ This is a template for Next.js projects using Drizzle ORM, self hosted PostgreSQL for development and Neon DB for production.
4
+
5
+ ## Basic Folder Structure
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
25
+
26
+ 1. Install the packages:
27
+
28
+ ```bash
29
+ bun install
30
+ ```
31
+
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:
45
+
46
+ ```bash
47
+ # development
48
+ 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
73
+ ```
74
+
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
86
+
87
+ # production
88
+ bun --env-file=.env.production server/test-connection/index.ts
89
+ ```
90
+
91
+ ## Features
92
+
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
+
102
+ ## WIP
103
+
104
+ 1. Nothing in flight
105
+
106
+ ## Using this as a package
107
+
108
+ 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.
109
+
110
+ ### 1. Install
111
+
112
+ ```bash
113
+ bun add @xk2800/nextjs-template
114
+ ```
115
+
116
+ 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).
117
+
118
+ ### 2. Enable `transpilePackages`
119
+
120
+ UI components ship as raw `.tsx` source so `'use client'` boundaries survive. Add this to your `next.config.ts`:
121
+
122
+ ```ts
123
+ const nextConfig: NextConfig = {
124
+ transpilePackages: ["@xk2800/nextjs-template"],
125
+ };
126
+ ```
127
+
128
+ ### 3. Tailwind theme + content scanning
129
+
130
+ 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";`:
131
+
132
+ ```css
133
+ @import "tailwindcss";
134
+ @import "@xk2800/nextjs-template/styles/theme.css";
135
+ @source "../node_modules/@xk2800/nextjs-template";
136
+ ```
137
+
138
+ 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.
139
+
140
+ ### 4. Adding a social provider (e.g. Apple) in one project only
141
+
142
+ `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.
143
+
144
+ **a. Create your own `server/auth.ts`** that wraps the factory:
145
+
146
+ ```ts
147
+ // server/auth.ts (your project, not the package)
148
+ import { createAuth } from "@xk2800/nextjs-template/auth";
149
+
150
+ export const auth = createAuth({
151
+ socialProviders: {
152
+ apple: {
153
+ clientId: process.env.APPLE_CLIENT_ID!, // your Apple Services ID
154
+ clientSecret: process.env.APPLE_CLIENT_SECRET!, // generated signed JWT, not a static secret
155
+ appBundleIdentifier: process.env.APPLE_APP_BUNDLE_IDENTIFIER!, // only if you also support native/iOS sign-in
156
+ },
157
+ },
158
+ });
159
+ ```
160
+
161
+ 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.
162
+
163
+ **b. Point your API route at your own `auth`, not the package's:**
164
+
165
+ ```ts
166
+ // app/api/auth/[...all]/route.ts
167
+ import { auth } from "@/server/auth"; // your local file from step a — not "@xk2800/nextjs-template/auth"
168
+ import { toNextJsHandler } from "better-auth/next-js";
169
+
170
+ export const { GET, POST } = toNextJsHandler(auth);
171
+ ```
172
+
173
+ 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.
174
+
175
+ **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:
176
+
177
+ ```tsx
178
+ "use client";
179
+ import { Button } from "@xk2800/nextjs-template/components/ui/button";
180
+ import { authClient } from "@xk2800/nextjs-template/auth-client";
181
+
182
+ export function AppleSignInButton({ callbackUrl }: { callbackUrl?: string }) {
183
+ return (
184
+ <Button variant="outline" className="w-full" onClick={() => authClient.signIn.social({ provider: "apple", callbackURL: callbackUrl || "/dashboard" })}>
185
+ Sign in with Apple
186
+ </Button>
187
+ );
188
+ }
189
+ ```
190
+
191
+ Nothing else needs to change — `middleware.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.
192
+
193
+ #### Disabling Google or email/password instead
194
+
195
+ 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:
196
+
197
+ ```bash
198
+ # .env.* in your project (not the package) — either or both, default is true
199
+ AUTH_ENABLE_GOOGLE=false
200
+ AUTH_ENABLE_EMAIL_PASSWORD=false
201
+ ```
202
+
203
+ 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.
204
+
205
+ **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:
206
+
207
+ - Both flags `false`, no override → throws. There would be zero ways to sign in.
208
+ - Both flags `false`, `createAuth({ socialProviders: { apple: {...} } })` → works fine. Apple is your only provider, and that's a valid config.
209
+
210
+ 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.
211
+
212
+ #### Enabling Google One Tap
213
+
214
+ [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.
215
+
216
+ ```bash
217
+ # .env.* in your project — both default to false
218
+ AUTH_ENABLE_ONE_TAP=true
219
+ NEXT_PUBLIC_AUTH_ENABLE_ONE_TAP=true
220
+
221
+ # needed either way — same OAuth client as AUTH_GOOGLE_ID, but public
222
+ NEXT_PUBLIC_GOOGLE_CLIENT_ID=
223
+ ```
224
+
225
+ 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.
226
+
227
+ Mount the shipped component once, anywhere it should be able to prompt (root layout is typical):
228
+
229
+ ```tsx
230
+ import OneTap from "@xk2800/nextjs-template/components/auth/oneTap";
231
+ ```
232
+
233
+ `<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.
234
+
235
+ 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.
236
+
237
+ ### 5. Import what you need
238
+
239
+ ```ts
240
+ // auth
241
+ import { auth } from "@xk2800/nextjs-template/auth";
242
+ import { requireAuth, hasRole } from "@xk2800/nextjs-template/auth/helpers";
243
+ import { authClient, useSession } from "@xk2800/nextjs-template/auth-client";
244
+
245
+ // db
246
+ import { db } from "@xk2800/nextjs-template/db";
247
+ import { users, sessions } from "@xk2800/nextjs-template/db/schema";
248
+
249
+ // server-only query helpers (each throws if imported into a Client Component)
250
+ import { getAdminStats } from "@xk2800/nextjs-template/admin/queries";
251
+ import { getUserSessions } from "@xk2800/nextjs-template/sessions/queries";
252
+ import { logActivity } from "@xk2800/nextjs-template/activity/logger";
253
+ import { getUserActivityLogs, getAllActivityLogs } from "@xk2800/nextjs-template/activity/queries";
254
+
255
+ // config, types, isomorphic helpers
256
+ import { config } from "@xk2800/nextjs-template/config/env";
257
+ import { LoginSchema } from "@xk2800/nextjs-template/types/auth/loginSchema";
258
+ import { SignupSchema } from "@xk2800/nextjs-template/types/auth/signupSchema";
259
+ import { formatDate, formatDateTime, getUserInitials } from "@xk2800/nextjs-template/lib/formatters";
260
+
261
+ // ui primitives (raw source, any file under components/ui)
262
+ import { Button } from "@xk2800/nextjs-template/components/ui/button";
263
+
264
+ // auth UI
265
+ import AuthCard from "@xk2800/nextjs-template/components/auth/authCard";
266
+ import LogoutButtons from "@xk2800/nextjs-template/components/auth/logoutButtons";
267
+ import EmailPasswordLogin from "@xk2800/nextjs-template/components/auth/emailPasswordLogin";
268
+ import EmailPasswordSignup from "@xk2800/nextjs-template/components/auth/emailPasswordSignup";
269
+ import OneTap from "@xk2800/nextjs-template/components/auth/oneTap"; // no-ops unless NEXT_PUBLIC_AUTH_ENABLE_ONE_TAP=true
270
+
271
+ // dashboard UI (each takes data as props — fetch with the helpers above in your page, then pass down)
272
+ import ProfileCard from "@xk2800/nextjs-template/components/dashboard/profileCard";
273
+ import AccountDetailsCard from "@xk2800/nextjs-template/components/dashboard/accountDetailsCard";
274
+ import SessionsCard from "@xk2800/nextjs-template/components/dashboard/sessionsCard";
275
+ import AdminSection, { type AdminStats } from "@xk2800/nextjs-template/components/dashboard/adminSection";
276
+ import ActivityLogsCard from "@xk2800/nextjs-template/components/dashboard/activityLogsCard";
277
+ import DashboardHeader, { type DashboardNavLink } from "@xk2800/nextjs-template/components/dashboard/dashboardHeader"; // brandName, brandHref, navLinks are optional props
278
+
279
+ // optional feature UI (needs the matching optional peer dep)
280
+ import { ThemeProvider } from "@xk2800/nextjs-template/components/providers/theme-provider"; // next-themes
281
+ import { ThemeToggle } from "@xk2800/nextjs-template/components/theme/theme-toggle"; // next-themes
282
+ import { SendEmailButton } from "@xk2800/nextjs-template/components/email/send-email-button";
283
+ import { EmailTemplate } from "@xk2800/nextjs-template/components/email/email-template"; // @react-email/components
284
+ ```
285
+
286
+ ### 6. Not importable — copy these patterns instead
287
+
288
+ `app/api/auth/[...all]/route.ts` and `middleware.ts` are Next.js file-convention code, not library exports. Copy the pattern into your own project:
289
+
290
+ ```ts
291
+ // app/api/auth/[...all]/route.ts
292
+ import { auth } from "@xk2800/nextjs-template/auth";
293
+ import { toNextJsHandler } from "better-auth/next-js";
294
+
295
+ export const { GET, POST } = toNextJsHandler(auth);
296
+ ```
297
+
298
+ ### 7. Database migrations
299
+
300
+ 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:
301
+
302
+ ```ts
303
+ // server/db/schema.ts
304
+ export * from "@xk2800/nextjs-template/db/schema";
305
+ ```
306
+
307
+ 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):
308
+
309
+ ```ts
310
+ // drizzle.config.ts
311
+ import { defineConfig } from "drizzle-kit";
312
+
313
+ export default defineConfig({
314
+ out: "./server/drizzle",
315
+ schema: "./server/db/schema.ts",
316
+ dialect: "postgresql",
317
+ dbCredentials: { url: process.env.DATABASE_URL! },
318
+ });
319
+ ```
320
+
321
+ ### 8. Secrets management with Doppler (optional)
322
+
323
+ 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:
324
+
325
+ ```bash
326
+ doppler login
327
+ doppler setup # in your project root, select/create a Doppler project + config
328
+ ```
329
+
330
+ Then wrap any command that needs your env vars with `doppler run --`, instead of relying on `.env.development`/`.env.local`:
331
+
332
+ ```bash
333
+ doppler run -- bun dev
334
+ doppler run -- bun run generate # drizzle-kit generate
335
+ doppler run -- bun run migrate # drizzle-kit migrate
336
+ doppler run -- next build
337
+ ```
338
+
339
+ 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.
@@ -0,0 +1,92 @@
1
+ import Link from 'next/link'
2
+ import { Separator } from "../ui/separator"
3
+ import SocialLogin from './socialLogin'
4
+ import EmailPasswordLogin from './emailPasswordLogin'
5
+ import EmailPasswordSignup from './emailPasswordSignup'
6
+ import PasskeyLogin from './passkeyLogin'
7
+
8
+ type Props = {
9
+ authCardTitle: string
10
+ authCardDescription: string
11
+ authCardAction: string
12
+ showSocials: boolean
13
+ showEmailPassword: boolean
14
+ variant?: 'login' | 'signup'
15
+ callbackUrl?: string
16
+ }
17
+
18
+ const authCard = ({ authCardTitle, authCardDescription, showSocials, showEmailPassword, variant = 'login', callbackUrl }: Props) => {
19
+ const toggleHref = callbackUrl
20
+ ? `${variant === 'signup' ? '/login' : '/signup'}?callbackUrl=${encodeURIComponent(callbackUrl)}`
21
+ : variant === 'signup'
22
+ ? '/login'
23
+ : '/signup'
24
+
25
+ return (
26
+ <div>
27
+ <h1 className="text-3xl font-bold tracking-tight">{authCardTitle}</h1>
28
+ <p className="mt-2 text-sm text-muted-foreground">{authCardDescription}</p>
29
+
30
+ <div className="mt-8 flex flex-col gap-4">
31
+ {showSocials && (
32
+ <div>
33
+ <SocialLogin
34
+ callbackUrl={callbackUrl}
35
+ label={variant === 'signup' ? 'Sign up with Google' : 'Sign in with Google'}
36
+ />
37
+ </div>
38
+ )}
39
+ {showSocials && showEmailPassword && (
40
+ <div className="flex items-center gap-2">
41
+ <Separator className="flex-1" />
42
+ <span className="text-xs text-muted-foreground">or continue with</span>
43
+ <Separator className="flex-1" />
44
+ </div>
45
+ )}
46
+ {showEmailPassword && (
47
+ variant === 'signup'
48
+ ? <EmailPasswordSignup callbackUrl={callbackUrl} />
49
+ : <EmailPasswordLogin callbackUrl={callbackUrl} />
50
+ )}
51
+
52
+ {variant === 'login' && (
53
+ <>
54
+ {(showSocials || showEmailPassword) && (
55
+ <div className="flex items-center gap-2">
56
+ <Separator className="flex-1" />
57
+ <span className="text-xs text-muted-foreground">or</span>
58
+ <Separator className="flex-1" />
59
+ </div>
60
+ )}
61
+ <PasskeyLogin callbackUrl={callbackUrl} />
62
+ </>
63
+ )}
64
+ </div>
65
+
66
+ {showEmailPassword && variant === 'signup' && (
67
+ <p className="mt-6 text-center text-xs text-muted-foreground">
68
+ By continuing you agree to the{' '}
69
+ <Link href="/terms" className="underline underline-offset-4 hover:text-foreground">Terms</Link>
70
+ {' '}and{' '}
71
+ <Link href="/privacy" className="underline underline-offset-4 hover:text-foreground">Privacy Policy</Link>.
72
+ </p>
73
+ )}
74
+
75
+ {showEmailPassword && (
76
+ <p className="mt-6 text-center text-sm text-muted-foreground">
77
+ {variant === 'signup' ? (
78
+ <>Already have an account?{' '}
79
+ <Link href={toggleHref} className="font-medium text-foreground underline underline-offset-4">Log in</Link>
80
+ </>
81
+ ) : (
82
+ <>Don&apos;t have an account?{' '}
83
+ <Link href={toggleHref} className="font-medium text-foreground underline underline-offset-4">Sign up</Link>
84
+ </>
85
+ )}
86
+ </p>
87
+ )}
88
+ </div>
89
+ )
90
+ }
91
+
92
+ export default authCard
@@ -0,0 +1,46 @@
1
+ import type { ReactNode } from "react"
2
+ import { Check } from "lucide-react"
3
+
4
+ import { Logo } from "@/components/site/logo"
5
+ import { ThemeToggle } from "@/components/theme/theme-toggle"
6
+
7
+ type Props = {
8
+ headline: string
9
+ bullets: string[]
10
+ children: ReactNode
11
+ }
12
+
13
+ export function AuthShell({ headline, bullets, children }: Props) {
14
+ return (
15
+ <div className="grid min-h-screen lg:grid-cols-2">
16
+ <div className="relative hidden flex-col justify-center border-r border-border/60 bg-secondary/30 p-10 lg:flex">
17
+ <div className="absolute top-10 left-10">
18
+ <Logo />
19
+ </div>
20
+ <div>
21
+ <h1 className="text-3xl font-bold tracking-tight text-balance">{headline}</h1>
22
+ <ul className="mt-6 flex flex-col gap-3">
23
+ {bullets.map((bullet) => (
24
+ <li key={bullet} className="flex items-start gap-2.5 text-sm text-muted-foreground">
25
+ <Check className="mt-0.5 size-4 shrink-0 text-green-600 dark:text-green-500" />
26
+ {bullet}
27
+ </li>
28
+ ))}
29
+ </ul>
30
+ </div>
31
+ </div>
32
+
33
+ <div className="flex flex-col p-6 sm:p-10">
34
+ <div className="flex items-center justify-between lg:justify-end">
35
+ <div className="lg:hidden">
36
+ <Logo />
37
+ </div>
38
+ <ThemeToggle />
39
+ </div>
40
+ <div className="flex flex-1 items-center justify-center py-10">
41
+ <div className="w-full max-w-sm">{children}</div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ )
46
+ }
@@ -0,0 +1,52 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useSession } from '../../lib/auth-client'
5
+ import { getVisitorId } from '../../lib/device-fingerprint'
6
+
7
+ // Mount once (e.g. in the root layout). When a session is present it computes
8
+ // a FingerprintJS device id and hands it to /api/device-check, which emails
9
+ // the user if the account has never signed in from this device before.
10
+ //
11
+ // Driven by "a session exists", not by the sign-in call, so it covers
12
+ // email/password, Google OAuth and One Tap without touching any of them. The
13
+ // endpoint is idempotent — the alert only fires on the first ever POST for a
14
+ // given (user, device).
15
+ export default function DeviceCheck() {
16
+ const { data: session } = useSession()
17
+ const userId = session?.user?.id
18
+
19
+ useEffect(() => {
20
+ if (!userId) return
21
+
22
+ const run = async () => {
23
+ try {
24
+ if (sessionStorage.getItem('device-checked') === userId) return
25
+ } catch {
26
+ // No sessionStorage (private mode etc.) — fall through. The endpoint
27
+ // is idempotent, so the cost is at most one redundant POST per load.
28
+ }
29
+
30
+ const visitorId = await getVisitorId()
31
+ if (!visitorId) return
32
+
33
+ await fetch('/api/device-check', {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ visitorId }),
37
+ })
38
+
39
+ try {
40
+ sessionStorage.setItem('device-checked', userId)
41
+ } catch {
42
+ // ignore — see above
43
+ }
44
+ }
45
+
46
+ // Best-effort: fingerprinting or the network failing must never surface
47
+ // to the user mid-app.
48
+ run().catch(() => {})
49
+ }, [userId])
50
+
51
+ return null
52
+ }
@@ -0,0 +1,136 @@
1
+ 'use client'
2
+
3
+ import React, { useEffect, useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { useRouter } from 'next/navigation'
6
+ import { z } from 'zod'
7
+ import { Button } from '../ui/button'
8
+ import { Input } from '../ui/input'
9
+ import { PasswordInput } from '../ui/password-input'
10
+ import { Label } from '../ui/label'
11
+ import { authClient } from '../../lib/auth-client'
12
+ import { getVisitorId } from '../../lib/device-fingerprint'
13
+ import { LOGIN_REFERRER_COOKIE, DEVICE_FINGERPRINT_HEADER } from '../../lib/cookie-names'
14
+ import { LoginSchema } from '../../types/auth/loginSchema'
15
+ import { toast } from 'sonner'
16
+
17
+ type Props = {
18
+ callbackUrl?: string
19
+ }
20
+
21
+ // Derived from the schema (not hand-typed) so a field rename/add/remove
22
+ // there can't silently drift out of sync with the error state's shape here.
23
+ type FieldErrors = Partial<Record<keyof z.infer<typeof LoginSchema>, string>>
24
+
25
+ const EmailPasswordLogin = ({ callbackUrl }: Props) => {
26
+ const router = useRouter()
27
+ const [email, setEmail] = useState('')
28
+ const [password, setPassword] = useState('')
29
+ const [isSubmitting, setIsSubmitting] = useState(false)
30
+ const [fieldErrors, setFieldErrors] = useState<FieldErrors>({})
31
+
32
+ // Warm the fingerprint cache so the first sign-in submit doesn't wait on it.
33
+ useEffect(() => {
34
+ void getVisitorId()
35
+ }, [])
36
+
37
+ const handleSubmit = async (e: React.FormEvent) => {
38
+ e.preventDefault()
39
+
40
+ const result = LoginSchema.safeParse({ email, password })
41
+ if (!result.success) {
42
+ const formatted = z.flattenError(result.error).fieldErrors
43
+ setFieldErrors({
44
+ email: formatted.email?.[0],
45
+ password: formatted.password?.[0],
46
+ })
47
+ return
48
+ }
49
+ setFieldErrors({})
50
+
51
+ setIsSubmitting(true)
52
+
53
+ // Overwrite any stale value left by an abandoned social sign-in attempt
54
+ // (see socialLogin.tsx) — this request's own referer header is already
55
+ // correct for email/password, but the login-activity log always prefers
56
+ // the cookie when present, so it needs to reflect this flow instead.
57
+ document.cookie = `${LOGIN_REFERRER_COOKIE}=${encodeURIComponent(window.location.href)}; path=/; max-age=300; samesite=lax`
58
+
59
+ try {
60
+ const visitorId = await getVisitorId()
61
+ const { data, error } = await authClient.signIn.email({
62
+ email,
63
+ password,
64
+ callbackURL: callbackUrl || '/dashboard',
65
+ fetchOptions: visitorId
66
+ ? { headers: { [DEVICE_FINGERPRINT_HEADER]: visitorId } }
67
+ : undefined,
68
+ })
69
+
70
+ if (error) {
71
+ toast.error(error.message || 'Failed to sign in')
72
+ return
73
+ }
74
+
75
+ // Account has TOTP enabled: no session yet, finish on the 2FA page.
76
+ if ((data as { twoFactorRedirect?: boolean } | null)?.twoFactorRedirect) {
77
+ const target = callbackUrl || '/dashboard'
78
+ router.push(`/login/2fa?callbackUrl=${encodeURIComponent(target)}`)
79
+ }
80
+ } catch (error) {
81
+ toast.error(error instanceof Error ? error.message : 'Failed to sign in')
82
+ } finally {
83
+ setIsSubmitting(false)
84
+ }
85
+ }
86
+
87
+ return (
88
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
89
+ <div className="flex flex-col gap-2">
90
+ <Label htmlFor="email" className={fieldErrors.email ? 'text-destructive' : undefined}>
91
+ Email
92
+ </Label>
93
+ <Input
94
+ id="email"
95
+ type="email"
96
+ autoComplete="email"
97
+ aria-invalid={!!fieldErrors.email}
98
+ aria-describedby={fieldErrors.email ? 'email-error' : undefined}
99
+ value={email}
100
+ onChange={(e) => {
101
+ setEmail(e.target.value)
102
+ if (fieldErrors.email) setFieldErrors((prev) => ({ ...prev, email: undefined }))
103
+ }}
104
+ />
105
+ {fieldErrors.email && <p id="email-error" className="text-sm text-destructive">{fieldErrors.email}</p>}
106
+ </div>
107
+ <div className="flex flex-col gap-2">
108
+ <div className="flex items-center justify-between">
109
+ <Label htmlFor="password" className={fieldErrors.password ? 'text-destructive' : undefined}>
110
+ Password
111
+ </Label>
112
+ <Link href="/forgot-password" className="text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline">
113
+ Forgot password?
114
+ </Link>
115
+ </div>
116
+ <PasswordInput
117
+ id="password"
118
+ autoComplete="current-password"
119
+ aria-invalid={!!fieldErrors.password}
120
+ aria-describedby={fieldErrors.password ? 'password-error' : undefined}
121
+ value={password}
122
+ onChange={(e) => {
123
+ setPassword(e.target.value)
124
+ if (fieldErrors.password) setFieldErrors((prev) => ({ ...prev, password: undefined }))
125
+ }}
126
+ />
127
+ {fieldErrors.password && <p id="password-error" className="text-sm text-destructive">{fieldErrors.password}</p>}
128
+ </div>
129
+ <Button type="submit" className="w-full" disabled={isSubmitting}>
130
+ {isSubmitting ? 'Signing in...' : 'Sign in'}
131
+ </Button>
132
+ </form>
133
+ )
134
+ }
135
+
136
+ export default EmailPasswordLogin