@liedsonc/core-auth-kit 2.1.4 → 2.1.7
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 +802 -0
- package/dist/components/auth-card.d.ts +9 -0
- package/dist/components/auth-card.d.ts.map +1 -0
- package/dist/components/auth-card.js +9 -0
- package/dist/components/auth-form.d.ts +10 -0
- package/dist/components/auth-form.d.ts.map +1 -0
- package/dist/components/auth-form.js +21 -0
- package/dist/components/error-message.d.ts +6 -0
- package/dist/components/error-message.d.ts.map +1 -0
- package/dist/components/error-message.js +8 -0
- package/dist/components/form-field.d.ts +9 -0
- package/dist/components/form-field.d.ts.map +1 -0
- package/dist/components/form-field.js +7 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/{components/index.ts → dist/components/index.js} +8 -8
- package/dist/components/loading-spinner.d.ts +5 -0
- package/dist/components/loading-spinner.d.ts.map +1 -0
- package/dist/components/loading-spinner.js +6 -0
- package/dist/components/oauth-buttons.d.ts +11 -0
- package/dist/components/oauth-buttons.d.ts.map +1 -0
- package/dist/components/oauth-buttons.js +14 -0
- package/dist/components/password-input.d.ts +8 -0
- package/dist/components/password-input.d.ts.map +1 -0
- package/dist/components/password-input.js +36 -0
- package/dist/components/success-message.d.ts +6 -0
- package/dist/components/success-message.d.ts.map +1 -0
- package/dist/components/success-message.js +8 -0
- package/dist/components/ui/button.d.ts +12 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +33 -0
- package/dist/components/ui/card.d.ts +9 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +16 -0
- package/dist/components/ui/input.d.ts +4 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +8 -0
- package/dist/components/ui/label.d.ts +6 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +10 -0
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +13 -0
- package/dist/hooks/use-auth.d.ts +22 -0
- package/dist/hooks/use-auth.d.ts.map +1 -0
- package/dist/hooks/use-auth.js +112 -0
- package/dist/hooks/use-oauth.d.ts +6 -0
- package/dist/hooks/use-oauth.d.ts.map +1 -0
- package/dist/hooks/use-oauth.js +18 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/pages/forgot-password/index.d.ts +2 -0
- package/dist/pages/forgot-password/index.d.ts.map +1 -0
- package/dist/pages/forgot-password/index.js +30 -0
- package/dist/pages/index.d.ts +6 -0
- package/dist/pages/index.d.ts.map +1 -0
- package/{pages/index.ts → dist/pages/index.js} +5 -5
- package/dist/pages/login/index.d.ts +2 -0
- package/dist/pages/login/index.d.ts.map +1 -0
- package/dist/pages/login/index.js +45 -0
- package/dist/pages/register/index.d.ts +2 -0
- package/dist/pages/register/index.d.ts.map +1 -0
- package/dist/pages/register/index.js +59 -0
- package/dist/pages/reset-password/index.d.ts +2 -0
- package/dist/pages/reset-password/index.d.ts.map +1 -0
- package/dist/pages/reset-password/index.js +48 -0
- package/dist/pages/verify-email/index.d.ts +2 -0
- package/dist/pages/verify-email/index.d.ts.map +1 -0
- package/dist/pages/verify-email/index.js +47 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +60 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +5 -0
- package/package.json +15 -11
- package/components/auth-card.tsx +0 -49
- package/components/auth-form.tsx +0 -53
- package/components/error-message.tsx +0 -24
- package/components/form-field.tsx +0 -39
- package/components/loading-spinner.tsx +0 -19
- package/components/oauth-buttons.tsx +0 -49
- package/components/password-input.tsx +0 -93
- package/components/success-message.tsx +0 -24
- package/components/ui/button.tsx +0 -52
- package/components/ui/card.tsx +0 -75
- package/components/ui/input.tsx +0 -21
- package/components/ui/label.tsx +0 -25
- package/context.tsx +0 -26
- package/hooks/use-auth.ts +0 -131
- package/hooks/use-oauth.ts +0 -25
- package/index.ts +0 -28
- package/pages/forgot-password/index.tsx +0 -83
- package/pages/login/index.tsx +0 -119
- package/pages/register/index.tsx +0 -149
- package/pages/reset-password/index.tsx +0 -133
- package/pages/verify-email/index.tsx +0 -143
- package/types/index.ts +0 -34
- package/utils.ts +0 -6
package/README.md
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
# @liedsonc/core-auth-kit
|
|
2
|
+
|
|
3
|
+
This is a core authentication UI package for Next.js for lazy people like me 😂.
|
|
4
|
+
|
|
5
|
+
I don't want to make Login, Register, and Forgot Password page for every project 🤢 (sooooo boring).
|
|
6
|
+
That's why I created this package that makes all UI using my fav tech's `shadcn/ui`, `Tailwind CSS`, `Resend` for mailing.
|
|
7
|
+
Make my auth UI with a single command, and it is also customizable for your project. Just install the package and connect it with the services keys.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- 🔐 **Complete Auth Flows** - Login, Register, Forgot Password, Reset Password, Email Verification
|
|
13
|
+
- 🎨 **Beautiful UI** - Built with shadcn/ui components, fully themeable and accessible
|
|
14
|
+
- 🔌 **Backend Agnostic** - Works with any authentication backend via `AuthClient` interface
|
|
15
|
+
- 📱 **Mobile-First** - Responsive design that works on all devices
|
|
16
|
+
- ♿ **Accessible** - WCAG compliant with proper ARIA attributes and keyboard navigation
|
|
17
|
+
- 🎯 **Type-Safe** - Full TypeScript support with strict typing
|
|
18
|
+
- 🚀 **Zero Config** - Works out-of-the-box with sensible defaults
|
|
19
|
+
- 🎨 **Customizable** - Override components, styles, and behavior as needed
|
|
20
|
+
- 🌙 **Dark Mode** - Built-in dark mode support via Tailwind CSS
|
|
21
|
+
- ⚙️ **Environment Configurable** - OAuth providers and settings via environment variables
|
|
22
|
+
- 📧 **Email Integration** - Built-in Resend email service support for verification and password reset emails
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
For plug-and-play, run **one command** from your Next.js app root (see [Plug and Play](#plug-and-play-recommended)):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @liedsonc/core-auth-kit init
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install the package yourself; all UI dependencies are installed automatically:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @liedsonc/core-auth-kit
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Peer Dependencies
|
|
39
|
+
|
|
40
|
+
The following peer dependencies are required but should already be installed in your Next.js project:
|
|
41
|
+
|
|
42
|
+
- `next` (^15.0.0) - Next.js framework
|
|
43
|
+
- `react` (^19.0.0) - React library
|
|
44
|
+
- `react-dom` (^19.0.0) - React DOM library
|
|
45
|
+
|
|
46
|
+
If you're not using Next.js or these aren't installed, install them:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install next react react-dom
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Note:** The following dependencies are automatically installed with the package:
|
|
53
|
+
- `@radix-ui/react-label` - For accessible form labels
|
|
54
|
+
- `@radix-ui/react-slot` - For component composition
|
|
55
|
+
- `class-variance-authority` - For component variants
|
|
56
|
+
- `clsx` - For conditional class names
|
|
57
|
+
- `tailwind-merge` - For merging Tailwind classes
|
|
58
|
+
|
|
59
|
+
### Email Service (Optional)
|
|
60
|
+
|
|
61
|
+
For email verification and password reset functionality, install Resend:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install resend
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### shadcn/ui Components
|
|
68
|
+
|
|
69
|
+
The package includes shadcn/ui components by default. Ensure your project has the shadcn/ui setup:
|
|
70
|
+
|
|
71
|
+
1. Install shadcn/ui components (if not already installed):
|
|
72
|
+
```bash
|
|
73
|
+
npx shadcn@latest add button input card label
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
2. Ensure your `tailwind.config.ts` includes the auth-ui styles:
|
|
77
|
+
```ts
|
|
78
|
+
import type { Config } from "tailwindcss";
|
|
79
|
+
|
|
80
|
+
const config: Config = {
|
|
81
|
+
content: [
|
|
82
|
+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
83
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
84
|
+
"./node_modules/@liedsonc/core-auth-kit/**/*.{js,ts,jsx,tsx}",
|
|
85
|
+
],
|
|
86
|
+
// ... rest of config
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
3. Import the styles in your root layout:
|
|
91
|
+
```tsx
|
|
92
|
+
import "@liedsonc/core-auth-kit/styles";
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Plug and Play (Recommended)
|
|
96
|
+
|
|
97
|
+
From your Next.js app root, a single command installs the package (if needed) and creates all auth pages, layout, config, and optional API route stubs:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx @liedsonc/core-auth-kit init
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The CLI installs `@liedsonc/core-auth-kit` when it is not already in your project, then scaffolds the files. Use `--force` to overwrite existing files. The CLI detects `app` vs `src/app` and `lib` vs `src/lib` automatically.
|
|
104
|
+
|
|
105
|
+
**Next steps:**
|
|
106
|
+
|
|
107
|
+
1. Implement `lib/auth-client.ts` (or `src/lib/auth-client.ts`) with your backend (e.g. Supabase, custom API). The stub returns "NOT_IMPLEMENTED" until you replace it.
|
|
108
|
+
2. Copy `.env.example` to `.env.local` and set your environment variables.
|
|
109
|
+
3. If your `AuthClient` calls `/api/auth/*`, implement the scaffolded API routes in `app/api/auth/` (or `src/app/api/auth/`); each stub returns 501 until you wire it to your backend.
|
|
110
|
+
4. Ensure Tailwind content includes the package and you import `@liedsonc/core-auth-kit/styles` in your root layout (see above).
|
|
111
|
+
|
|
112
|
+
After that, auth routes are available at `/login`, `/register`, `/forgot-password`, `/reset-password`, and `/verify-email`. The only required integration point is your `AuthClient` implementation; the rest is wiring and optional API handlers.
|
|
113
|
+
|
|
114
|
+
### Scaffolded files
|
|
115
|
+
|
|
116
|
+
| Path | Purpose | What to do |
|
|
117
|
+
|------|---------|------------|
|
|
118
|
+
| `app/(auth)/layout.tsx` | Wraps auth routes with `AuthUIProvider` and config from `lib/auth-config.ts`. | Usually no change. |
|
|
119
|
+
| `app/(auth)/login/page.tsx` | Login page (and similar for register, forgot-password, reset-password, verify-email). | Usually no change. |
|
|
120
|
+
| `lib/auth-config.ts` | Builds `AuthUIConfig` (authClient, OAuth from env, redirects). | Edit only if you need custom redirects or OAuth config. |
|
|
121
|
+
| `lib/auth-client.ts` | **AuthClient implementation.** | **Replace the stub** with your real backend (Supabase, custom API, etc.). |
|
|
122
|
+
| `.env.example` | Example environment variables. | Copy to `.env.local` and fill in your values. |
|
|
123
|
+
| `app/api/auth/*/route.ts` | API route stubs (login, register, logout, forgot-password, reset-password, verify-email, session). | Implement each handler to call your backend; stubs return 501 until then. |
|
|
124
|
+
|
|
125
|
+
## Environment Variables
|
|
126
|
+
|
|
127
|
+
Create a `.env.local` file (or `.env`) with the following variables:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
131
|
+
|
|
132
|
+
NEXT_PUBLIC_AUTH_REDIRECT_AFTER_LOGIN=/
|
|
133
|
+
NEXT_PUBLIC_AUTH_REDIRECT_AFTER_REGISTER=/login
|
|
134
|
+
NEXT_PUBLIC_AUTH_REDIRECT_AFTER_RESET=/login
|
|
135
|
+
|
|
136
|
+
NEXT_PUBLIC_OAUTH_GOOGLE_ENABLED=false
|
|
137
|
+
NEXT_PUBLIC_OAUTH_GOOGLE_CLIENT_ID=your_google_client_id
|
|
138
|
+
NEXT_PUBLIC_OAUTH_GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/google
|
|
139
|
+
OAUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret
|
|
140
|
+
|
|
141
|
+
NEXT_PUBLIC_OAUTH_APPLE_ENABLED=false
|
|
142
|
+
NEXT_PUBLIC_OAUTH_APPLE_CLIENT_ID=your_apple_client_id
|
|
143
|
+
NEXT_PUBLIC_OAUTH_APPLE_REDIRECT_URI=http://localhost:3000/api/auth/apple
|
|
144
|
+
OAUTH_APPLE_CLIENT_SECRET=your_apple_client_secret
|
|
145
|
+
|
|
146
|
+
RESEND_API_KEY=re_your_resend_api_key
|
|
147
|
+
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
|
148
|
+
RESEND_FROM_NAME=Your App Name
|
|
149
|
+
NEXT_PUBLIC_APP_NAME=Your App Name
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Environment Variable Reference
|
|
153
|
+
|
|
154
|
+
| Variable | Description | Required | Default |
|
|
155
|
+
|----------|-------------|----------|---------|
|
|
156
|
+
| `NEXT_PUBLIC_APP_URL` | Your application base URL | No | `http://localhost:3000` |
|
|
157
|
+
| `NEXT_PUBLIC_AUTH_REDIRECT_AFTER_LOGIN` | Redirect URL after successful login | No | `/` |
|
|
158
|
+
| `NEXT_PUBLIC_AUTH_REDIRECT_AFTER_REGISTER` | Redirect URL after registration | No | `/login` |
|
|
159
|
+
| `NEXT_PUBLIC_AUTH_REDIRECT_AFTER_RESET` | Redirect URL after password reset | No | `/login` |
|
|
160
|
+
| `NEXT_PUBLIC_OAUTH_GOOGLE_ENABLED` | Enable/disable Google OAuth | No | `false` |
|
|
161
|
+
| `NEXT_PUBLIC_OAUTH_GOOGLE_CLIENT_ID` | Google OAuth Client ID | Yes (if Google enabled) | - |
|
|
162
|
+
| `NEXT_PUBLIC_OAUTH_GOOGLE_REDIRECT_URI` | Google OAuth redirect URI | Yes (if Google enabled) | - |
|
|
163
|
+
| `OAUTH_GOOGLE_CLIENT_SECRET` | Google OAuth Client Secret (server-only) | Yes (if Google enabled) | - |
|
|
164
|
+
| `NEXT_PUBLIC_OAUTH_APPLE_ENABLED` | Enable/disable Apple OAuth | No | `false` |
|
|
165
|
+
| `NEXT_PUBLIC_OAUTH_APPLE_CLIENT_ID` | Apple OAuth Client ID | Yes (if Apple enabled) | - |
|
|
166
|
+
| `NEXT_PUBLIC_OAUTH_APPLE_REDIRECT_URI` | Apple OAuth redirect URI | Yes (if Apple enabled) | - |
|
|
167
|
+
| `OAUTH_APPLE_CLIENT_SECRET` | Apple OAuth Client Secret (server-only) | Yes (if Apple enabled) | - |
|
|
168
|
+
| `RESEND_API_KEY` | Resend API key for sending emails | Yes (if using email features) | - |
|
|
169
|
+
| `RESEND_FROM_EMAIL` | Email address to send from (must be verified in Resend) | Yes (if using email features) | - |
|
|
170
|
+
| `RESEND_FROM_NAME` | Display name for email sender | No | - |
|
|
171
|
+
| `NEXT_PUBLIC_APP_NAME` | Your application name (used in emails) | No | `App` |
|
|
172
|
+
|
|
173
|
+
**Note:** Variables prefixed with `NEXT_PUBLIC_` are exposed to the browser. Secrets should NOT use this prefix.
|
|
174
|
+
|
|
175
|
+
## Quick Start
|
|
176
|
+
|
|
177
|
+
If you ran `npx @liedsonc/core-auth-kit init`, the layout and pages already exist; go to **1. Implement AuthClient** and replace the stub in `lib/auth-client.ts` with your backend. Otherwise, follow steps 1–3 below.
|
|
178
|
+
|
|
179
|
+
### 1. Implement AuthClient
|
|
180
|
+
|
|
181
|
+
Create an `AuthClient` that connects to your backend (or replace the stub in `lib/auth-client.ts` if you used init):
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import type { AuthClient } from "@liedsonc/core-auth-kit";
|
|
185
|
+
|
|
186
|
+
export const myAuthClient: AuthClient = {
|
|
187
|
+
async login(email, password) {
|
|
188
|
+
const response = await fetch("/api/auth/login", {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "Content-Type": "application/json" },
|
|
191
|
+
body: JSON.stringify({ email, password }),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
error: { code: "INVALID_CREDENTIALS", message: "Invalid email or password" },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { success: true };
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
async register(email, password) {
|
|
205
|
+
const response = await fetch("/api/auth/register", {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: { "Content-Type": "application/json" },
|
|
208
|
+
body: JSON.stringify({ email, password }),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const data = await response.json();
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
error: { code: data.code || "REGISTRATION_FAILED", message: data.message },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { success: true };
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async logout() {
|
|
223
|
+
await fetch("/api/auth/logout", { method: "POST" });
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async forgotPassword(email) {
|
|
227
|
+
const response = await fetch("/api/auth/forgot-password", {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: { "Content-Type": "application/json" },
|
|
230
|
+
body: JSON.stringify({ email }),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return response.ok ? { success: true } : {
|
|
234
|
+
success: false,
|
|
235
|
+
error: { code: "FAILED", message: "Failed to send reset email" },
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async resetPassword(token, newPassword) {
|
|
240
|
+
const response = await fetch("/api/auth/reset-password", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: { "Content-Type": "application/json" },
|
|
243
|
+
body: JSON.stringify({ token, newPassword }),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: { code: "INVALID_TOKEN", message: "Invalid or expired token" },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { success: true };
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async verifyEmail(token) {
|
|
257
|
+
const response = await fetch("/api/auth/verify-email", {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: { "Content-Type": "application/json" },
|
|
260
|
+
body: JSON.stringify({ token }),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
return {
|
|
266
|
+
success: false,
|
|
267
|
+
error: { code: data.code || "INVALID", message: data.message },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { success: true };
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
async getSession() {
|
|
275
|
+
const response = await fetch("/api/auth/session");
|
|
276
|
+
if (!response.ok) return null;
|
|
277
|
+
return response.json();
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 2. Setup Provider with Environment Variables
|
|
283
|
+
|
|
284
|
+
Create a utility to load OAuth config from environment variables:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// lib/oauth-config.ts
|
|
288
|
+
import type { OAuthProvider } from "@liedsonc/core-auth-kit";
|
|
289
|
+
|
|
290
|
+
export interface OAuthProviderConfig {
|
|
291
|
+
provider: OAuthProvider;
|
|
292
|
+
enabled: boolean;
|
|
293
|
+
clientId?: string;
|
|
294
|
+
redirectUri?: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function getOAuthProvidersFromEnv(): OAuthProviderConfig[] {
|
|
298
|
+
const providers: OAuthProviderConfig[] = [];
|
|
299
|
+
|
|
300
|
+
if (process.env.NEXT_PUBLIC_OAUTH_GOOGLE_ENABLED === "true") {
|
|
301
|
+
providers.push({
|
|
302
|
+
provider: "google",
|
|
303
|
+
enabled: true,
|
|
304
|
+
clientId: process.env.NEXT_PUBLIC_OAUTH_GOOGLE_CLIENT_ID,
|
|
305
|
+
redirectUri: process.env.NEXT_PUBLIC_OAUTH_GOOGLE_REDIRECT_URI,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (process.env.NEXT_PUBLIC_OAUTH_APPLE_ENABLED === "true") {
|
|
310
|
+
providers.push({
|
|
311
|
+
provider: "apple",
|
|
312
|
+
enabled: true,
|
|
313
|
+
clientId: process.env.NEXT_PUBLIC_OAUTH_APPLE_CLIENT_ID,
|
|
314
|
+
redirectUri: process.env.NEXT_PUBLIC_OAUTH_APPLE_REDIRECT_URI,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return providers;
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Wrap your auth routes with `AuthUIProvider`:
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
// app/(auth)/layout.tsx
|
|
326
|
+
"use client";
|
|
327
|
+
|
|
328
|
+
import { AuthUIProvider } from "@liedsonc/core-auth-kit";
|
|
329
|
+
import { myAuthClient } from "@/lib/auth-client";
|
|
330
|
+
import { getOAuthProvidersFromEnv } from "@/lib/oauth-config";
|
|
331
|
+
|
|
332
|
+
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
|
333
|
+
return (
|
|
334
|
+
<AuthUIProvider
|
|
335
|
+
config={{
|
|
336
|
+
authClient: myAuthClient,
|
|
337
|
+
oauthProviders: getOAuthProvidersFromEnv(),
|
|
338
|
+
redirectAfterLogin: process.env.NEXT_PUBLIC_AUTH_REDIRECT_AFTER_LOGIN || "/",
|
|
339
|
+
redirectAfterRegister: process.env.NEXT_PUBLIC_AUTH_REDIRECT_AFTER_REGISTER || "/login",
|
|
340
|
+
redirectAfterReset: process.env.NEXT_PUBLIC_AUTH_REDIRECT_AFTER_RESET || "/login",
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
{children}
|
|
344
|
+
</AuthUIProvider>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 3. Create Auth Pages
|
|
350
|
+
|
|
351
|
+
Use the pre-built page components:
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
// app/(auth)/login/page.tsx
|
|
355
|
+
import { LoginPage } from "@liedsonc/core-auth-kit";
|
|
356
|
+
|
|
357
|
+
export default function Login() {
|
|
358
|
+
return <LoginPage />;
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
// app/(auth)/register/page.tsx
|
|
364
|
+
import { RegisterPage } from "@liedsonc/core-auth-kit";
|
|
365
|
+
|
|
366
|
+
export default function Register() {
|
|
367
|
+
return <RegisterPage />;
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
// app/(auth)/forgot-password/page.tsx
|
|
373
|
+
import { ForgotPasswordPage } from "@liedsonc/core-auth-kit";
|
|
374
|
+
|
|
375
|
+
export default function ForgotPassword() {
|
|
376
|
+
return <ForgotPasswordPage />;
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
// app/(auth)/reset-password/page.tsx
|
|
382
|
+
import { ResetPasswordPage } from "@liedsonc/core-auth-kit";
|
|
383
|
+
|
|
384
|
+
export default function ResetPassword() {
|
|
385
|
+
return <ResetPasswordPage />;
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
// app/(auth)/verify-email/page.tsx
|
|
391
|
+
import { VerifyEmailPage } from "@liedsonc/core-auth-kit";
|
|
392
|
+
|
|
393
|
+
export default function VerifyEmail() {
|
|
394
|
+
return <VerifyEmailPage />;
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Email Integration with Resend
|
|
399
|
+
|
|
400
|
+
The package includes utilities for sending verification and password reset emails via Resend. Here's how to integrate it:
|
|
401
|
+
|
|
402
|
+
### 1. Setup Resend
|
|
403
|
+
|
|
404
|
+
1. Sign up for a [Resend account](https://resend.com)
|
|
405
|
+
2. Get your API key from the dashboard
|
|
406
|
+
3. Verify your domain (or use Resend's test domain for development)
|
|
407
|
+
4. Add your credentials to `.env.local`:
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
RESEND_API_KEY=re_your_api_key_here
|
|
411
|
+
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
|
412
|
+
RESEND_FROM_NAME=Your App Name
|
|
413
|
+
NEXT_PUBLIC_APP_NAME=Your App Name
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### 2. Use Resend Email Service
|
|
417
|
+
|
|
418
|
+
Import and use the Resend email service in your AuthClient implementation:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// lib/auth-client.ts
|
|
422
|
+
import type { AuthClient } from "@liedsonc/core-auth-kit";
|
|
423
|
+
import { createResendEmailService } from "@/lib/resend-email";
|
|
424
|
+
import { randomBytes } from "crypto";
|
|
425
|
+
|
|
426
|
+
const emailService = createResendEmailService({
|
|
427
|
+
apiKey: process.env.RESEND_API_KEY!,
|
|
428
|
+
from: process.env.RESEND_FROM_EMAIL!,
|
|
429
|
+
fromName: process.env.RESEND_FROM_NAME,
|
|
430
|
+
appName: process.env.NEXT_PUBLIC_APP_NAME || "App",
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
|
434
|
+
|
|
435
|
+
export const myAuthClient: AuthClient = {
|
|
436
|
+
async register(email, password) {
|
|
437
|
+
// Your registration logic here
|
|
438
|
+
// ... save user to database
|
|
439
|
+
|
|
440
|
+
// Generate verification token
|
|
441
|
+
const token = randomBytes(32).toString("hex");
|
|
442
|
+
// Store token in database with expiration
|
|
443
|
+
// await db.verificationTokens.create({ email, token, expiresAt: ... });
|
|
444
|
+
|
|
445
|
+
// Send verification email
|
|
446
|
+
const verificationUrl = `${appUrl}/verify-email?token=${token}`;
|
|
447
|
+
const emailResult = await emailService.sendVerificationEmail({
|
|
448
|
+
email,
|
|
449
|
+
token,
|
|
450
|
+
verificationUrl,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (!emailResult.success) {
|
|
454
|
+
return {
|
|
455
|
+
success: false,
|
|
456
|
+
error: { code: "EMAIL_SEND_FAILED", message: "Failed to send verification email" },
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return { success: true };
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
async forgotPassword(email) {
|
|
464
|
+
// Generate reset token
|
|
465
|
+
const token = randomBytes(32).toString("hex");
|
|
466
|
+
// Store token in database with expiration (e.g., 1 hour)
|
|
467
|
+
// await db.resetTokens.create({ email, token, expiresAt: ... });
|
|
468
|
+
|
|
469
|
+
// Send password reset email
|
|
470
|
+
const resetUrl = `${appUrl}/reset-password?token=${token}`;
|
|
471
|
+
const emailResult = await emailService.sendPasswordResetEmail({
|
|
472
|
+
email,
|
|
473
|
+
token,
|
|
474
|
+
resetUrl,
|
|
475
|
+
expiresIn: "1 hour",
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
if (!emailResult.success) {
|
|
479
|
+
return {
|
|
480
|
+
success: false,
|
|
481
|
+
error: { code: "EMAIL_SEND_FAILED", message: "Failed to send reset email" },
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return { success: true };
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
// ... other AuthClient methods
|
|
489
|
+
};
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 3. Example Implementation
|
|
493
|
+
|
|
494
|
+
See `lib/auth-client-with-resend.ts` for a complete example implementation that includes:
|
|
495
|
+
- Token generation and validation
|
|
496
|
+
- Email sending with Resend
|
|
497
|
+
- Token expiration handling
|
|
498
|
+
- Error handling
|
|
499
|
+
|
|
500
|
+
### 4. Customizing Email Templates
|
|
501
|
+
|
|
502
|
+
The email templates are built into the `ResendEmailService` class. To customize them:
|
|
503
|
+
|
|
504
|
+
1. Extend the `ResendEmailService` class
|
|
505
|
+
2. Override the template methods (`getVerificationEmailTemplate`, `getPasswordResetEmailTemplate`)
|
|
506
|
+
3. Use your custom service instance
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
class CustomEmailService extends ResendEmailService {
|
|
510
|
+
private getVerificationEmailTemplate({ verificationUrl, appName }: { verificationUrl: string; appName: string }): string {
|
|
511
|
+
// Your custom HTML template
|
|
512
|
+
return `...`;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### 5. Email Service API
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
import { createResendEmailService, ResendEmailService } from "@/lib/resend-email";
|
|
521
|
+
|
|
522
|
+
// Create service instance
|
|
523
|
+
const emailService = createResendEmailService({
|
|
524
|
+
apiKey: process.env.RESEND_API_KEY!,
|
|
525
|
+
from: process.env.RESEND_FROM_EMAIL!,
|
|
526
|
+
fromName: "Your App",
|
|
527
|
+
appName: "Your App Name",
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Send verification email
|
|
531
|
+
await emailService.sendVerificationEmail({
|
|
532
|
+
email: "user@example.com",
|
|
533
|
+
token: "verification_token",
|
|
534
|
+
verificationUrl: "https://yourapp.com/verify-email?token=...",
|
|
535
|
+
appName: "Your App", // Optional, uses service default if not provided
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Send password reset email
|
|
539
|
+
await emailService.sendPasswordResetEmail({
|
|
540
|
+
email: "user@example.com",
|
|
541
|
+
token: "reset_token",
|
|
542
|
+
resetUrl: "https://yourapp.com/reset-password?token=...",
|
|
543
|
+
appName: "Your App", // Optional
|
|
544
|
+
expiresIn: "1 hour", // Optional, default is "1 hour"
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## API Reference
|
|
549
|
+
|
|
550
|
+
### Components
|
|
551
|
+
|
|
552
|
+
#### Pages
|
|
553
|
+
|
|
554
|
+
- **`LoginPage`** - Complete login page with email/password and OAuth
|
|
555
|
+
- **`RegisterPage`** - Registration page with password strength indicator
|
|
556
|
+
- **`ForgotPasswordPage`** - Password reset request page
|
|
557
|
+
- **`ResetPasswordPage`** - Password reset form (requires token query param)
|
|
558
|
+
- **`VerifyEmailPage`** - Email verification page (requires token query param)
|
|
559
|
+
|
|
560
|
+
#### UI Components
|
|
561
|
+
|
|
562
|
+
- **`AuthCard`** - Reusable card layout for auth pages
|
|
563
|
+
- **`AuthForm`** - Form wrapper with loading/error states
|
|
564
|
+
- **`FormField`** - Form field with label and error display
|
|
565
|
+
- **`PasswordInput`** - Password input with show/hide toggle and strength indicator
|
|
566
|
+
- **`OAuthButtons`** - OAuth provider buttons (Google, Apple)
|
|
567
|
+
- **`ErrorMessage`** - Error message display component
|
|
568
|
+
- **`SuccessMessage`** - Success message display component
|
|
569
|
+
- **`LoadingSpinner`** - Loading spinner component
|
|
570
|
+
|
|
571
|
+
### Hooks
|
|
572
|
+
|
|
573
|
+
#### `useAuth()`
|
|
574
|
+
|
|
575
|
+
Main authentication hook that provides all auth methods:
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
const {
|
|
579
|
+
login, // (email: string, password: string) => Promise<AuthResult>
|
|
580
|
+
register, // (email: string, password: string) => Promise<AuthResult>
|
|
581
|
+
logout, // () => Promise<void>
|
|
582
|
+
forgotPassword, // (email: string) => Promise<AuthResult>
|
|
583
|
+
resetPassword, // (token: string, newPassword: string) => Promise<AuthResult>
|
|
584
|
+
verifyEmail, // (token: string) => Promise<AuthResult>
|
|
585
|
+
session, // AuthSession | null
|
|
586
|
+
loadSession, // () => Promise<AuthSession | null>
|
|
587
|
+
loading, // boolean
|
|
588
|
+
error, // AuthError | null
|
|
589
|
+
clearError, // () => void
|
|
590
|
+
} = useAuth();
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
#### `useOAuth(onRedirect)`
|
|
594
|
+
|
|
595
|
+
OAuth authentication hook:
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
const { signIn, loadingProvider } = useOAuth(
|
|
599
|
+
(provider) => `/api/auth/${provider}`
|
|
600
|
+
);
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Types
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
interface AuthClient {
|
|
607
|
+
login: (email: string, password: string) => Promise<AuthResult>;
|
|
608
|
+
register: (email: string, password: string) => Promise<AuthResult>;
|
|
609
|
+
logout: () => Promise<void>;
|
|
610
|
+
forgotPassword: (email: string) => Promise<AuthResult>;
|
|
611
|
+
resetPassword: (token: string, newPassword: string) => Promise<AuthResult>;
|
|
612
|
+
verifyEmail: (token: string) => Promise<AuthResult>;
|
|
613
|
+
getSession?: () => Promise<AuthSession | null>;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
interface AuthUIConfig {
|
|
617
|
+
authClient: AuthClient;
|
|
618
|
+
oauthProviders?: { provider: OAuthProvider; enabled: boolean }[];
|
|
619
|
+
logo?: ReactNode;
|
|
620
|
+
redirectAfterLogin?: string;
|
|
621
|
+
redirectAfterRegister?: string;
|
|
622
|
+
redirectAfterReset?: string;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
type AuthResult =
|
|
626
|
+
| { success: true }
|
|
627
|
+
| { success: false; error: AuthError };
|
|
628
|
+
|
|
629
|
+
interface AuthError {
|
|
630
|
+
code: string;
|
|
631
|
+
message: string;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
type OAuthProvider = "google" | "apple";
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Customization
|
|
638
|
+
|
|
639
|
+
### Custom Logo
|
|
640
|
+
|
|
641
|
+
```tsx
|
|
642
|
+
<AuthUIProvider
|
|
643
|
+
config={{
|
|
644
|
+
authClient: myAuthClient,
|
|
645
|
+
logo: <img src="/logo.svg" alt="Logo" />,
|
|
646
|
+
}}
|
|
647
|
+
>
|
|
648
|
+
{children}
|
|
649
|
+
</AuthUIProvider>
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### OAuth Configuration via Environment Variables
|
|
653
|
+
|
|
654
|
+
Enable/disable OAuth providers and configure credentials via environment variables:
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
NEXT_PUBLIC_OAUTH_GOOGLE_ENABLED=true
|
|
658
|
+
NEXT_PUBLIC_OAUTH_GOOGLE_CLIENT_ID=your_client_id
|
|
659
|
+
NEXT_PUBLIC_OAUTH_GOOGLE_REDIRECT_URI=https://yourapp.com/api/auth/google
|
|
660
|
+
OAUTH_GOOGLE_CLIENT_SECRET=your_client_secret
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
The `getOAuthProvidersFromEnv()` utility automatically reads these and configures providers.
|
|
664
|
+
|
|
665
|
+
### Custom Redirects
|
|
666
|
+
|
|
667
|
+
Set redirects via environment variables or config:
|
|
668
|
+
|
|
669
|
+
```tsx
|
|
670
|
+
<AuthUIProvider
|
|
671
|
+
config={{
|
|
672
|
+
authClient: myAuthClient,
|
|
673
|
+
redirectAfterLogin: process.env.NEXT_PUBLIC_AUTH_REDIRECT_AFTER_LOGIN || "/dashboard",
|
|
674
|
+
redirectAfterRegister: process.env.NEXT_PUBLIC_AUTH_REDIRECT_AFTER_REGISTER || "/onboarding",
|
|
675
|
+
redirectAfterReset: process.env.NEXT_PUBLIC_AUTH_REDIRECT_AFTER_RESET || "/login?reset=success",
|
|
676
|
+
}}
|
|
677
|
+
>
|
|
678
|
+
{children}
|
|
679
|
+
</AuthUIProvider>
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Using Individual Components
|
|
683
|
+
|
|
684
|
+
You can also use components individually to build custom pages:
|
|
685
|
+
|
|
686
|
+
```tsx
|
|
687
|
+
import { AuthCard, AuthForm, FormField, PasswordInput, Button } from "@liedsonc/core-auth-kit";
|
|
688
|
+
|
|
689
|
+
export function CustomLoginPage() {
|
|
690
|
+
return (
|
|
691
|
+
<AuthCard title="Sign In">
|
|
692
|
+
<AuthForm onSubmit={handleSubmit}>
|
|
693
|
+
<FormField label="Email" htmlFor="email">
|
|
694
|
+
<Input id="email" type="email" />
|
|
695
|
+
</FormField>
|
|
696
|
+
<FormField label="Password" htmlFor="password">
|
|
697
|
+
<PasswordInput id="password" />
|
|
698
|
+
</FormField>
|
|
699
|
+
<Button type="submit">Sign In</Button>
|
|
700
|
+
</AuthForm>
|
|
701
|
+
</AuthCard>
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
## Production Setup
|
|
707
|
+
|
|
708
|
+
### 1. Environment Variables
|
|
709
|
+
|
|
710
|
+
Copy `.env.example` to `.env.local` and fill in your production values:
|
|
711
|
+
|
|
712
|
+
```bash
|
|
713
|
+
cp .env.example .env.local
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
**Important:** Never commit `.env.local` to version control. Add it to `.gitignore`.
|
|
717
|
+
|
|
718
|
+
### 2. OAuth Provider Setup
|
|
719
|
+
|
|
720
|
+
#### Google OAuth
|
|
721
|
+
|
|
722
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
723
|
+
2. Create a new project or select existing
|
|
724
|
+
3. Enable Google+ API
|
|
725
|
+
4. Create OAuth 2.0 credentials
|
|
726
|
+
5. Add authorized redirect URIs
|
|
727
|
+
6. Copy Client ID and Client Secret to `.env.local`
|
|
728
|
+
|
|
729
|
+
#### Apple OAuth
|
|
730
|
+
|
|
731
|
+
1. Go to [Apple Developer Portal](https://developer.apple.com/)
|
|
732
|
+
2. Create an App ID
|
|
733
|
+
3. Create a Services ID
|
|
734
|
+
4. Configure Sign in with Apple
|
|
735
|
+
5. Add redirect URIs
|
|
736
|
+
6. Copy Client ID and Client Secret to `.env.local`
|
|
737
|
+
|
|
738
|
+
### 3. Build and Deploy
|
|
739
|
+
|
|
740
|
+
```bash
|
|
741
|
+
npm run build
|
|
742
|
+
npm start
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
## Styling
|
|
746
|
+
|
|
747
|
+
The package uses Tailwind CSS and follows shadcn/ui conventions. Customize colors via CSS variables:
|
|
748
|
+
|
|
749
|
+
```css
|
|
750
|
+
:root {
|
|
751
|
+
--primary: 222.2 47.4% 11.2%;
|
|
752
|
+
--primary-foreground: 210 40% 98%;
|
|
753
|
+
/* ... other variables */
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
See [shadcn/ui theming](https://ui.shadcn.com/docs/theming) for details.
|
|
758
|
+
|
|
759
|
+
## Security Considerations
|
|
760
|
+
|
|
761
|
+
- **No User Enumeration**: Error messages don't reveal if an email exists
|
|
762
|
+
- **Secure Defaults**: Password inputs prevent autofill leaks
|
|
763
|
+
- **CSRF Protection**: Implement CSRF tokens in your `AuthClient`
|
|
764
|
+
- **Rate Limiting**: Implement rate limiting in your backend
|
|
765
|
+
- **Token Security**: Handle tokens securely in your `AuthClient`
|
|
766
|
+
- **Environment Variables**: Never expose secrets in client-side code (use `NEXT_PUBLIC_` prefix only for public values)
|
|
767
|
+
|
|
768
|
+
## Browser Support
|
|
769
|
+
|
|
770
|
+
- Chrome (latest)
|
|
771
|
+
- Firefox (latest)
|
|
772
|
+
- Safari (latest)
|
|
773
|
+
- Edge (latest)
|
|
774
|
+
|
|
775
|
+
## Project Structure
|
|
776
|
+
|
|
777
|
+
```
|
|
778
|
+
core-auth-kit/
|
|
779
|
+
├── auth-ui/ # Package source code
|
|
780
|
+
│ ├── components/ # UI components
|
|
781
|
+
│ ├── hooks/ # React hooks
|
|
782
|
+
│ ├── pages/ # Page components
|
|
783
|
+
│ ├── types/ # TypeScript types
|
|
784
|
+
│ ├── styles/ # CSS styles
|
|
785
|
+
│ └── index.ts # Main export
|
|
786
|
+
├── app/ # Example Next.js app
|
|
787
|
+
├── lib/ # Utilities (oauth-config, auth-client)
|
|
788
|
+
├── .env.example # Environment variables template
|
|
789
|
+
└── README.md # This file
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
## Contributing
|
|
793
|
+
|
|
794
|
+
Contributions are welcome! Please read our contributing guidelines first.
|
|
795
|
+
|
|
796
|
+
## License
|
|
797
|
+
|
|
798
|
+
MIT
|
|
799
|
+
|
|
800
|
+
## Support
|
|
801
|
+
|
|
802
|
+
For issues, questions, or contributions, please open an issue on GitHub.
|