autoworkflow 3.1.5 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Clerk Auth Skill
|
|
2
|
+
|
|
3
|
+
## Setup (Next.js App Router)
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
// middleware.ts
|
|
6
|
+
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
|
|
7
|
+
|
|
8
|
+
const isPublicRoute = createRouteMatcher([
|
|
9
|
+
'/',
|
|
10
|
+
'/sign-in(.*)',
|
|
11
|
+
'/sign-up(.*)',
|
|
12
|
+
'/api/webhooks(.*)',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export default clerkMiddleware((auth, req) => {
|
|
16
|
+
if (!isPublicRoute(req)) {
|
|
17
|
+
auth().protect();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const config = {
|
|
22
|
+
matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// app/layout.tsx
|
|
26
|
+
import { ClerkProvider } from '@clerk/nextjs';
|
|
27
|
+
|
|
28
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
29
|
+
return (
|
|
30
|
+
<ClerkProvider>
|
|
31
|
+
<html lang="en">
|
|
32
|
+
<body>{children}</body>
|
|
33
|
+
</html>
|
|
34
|
+
</ClerkProvider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
39
|
+
## Server Components
|
|
40
|
+
\`\`\`typescript
|
|
41
|
+
// Get auth in Server Components
|
|
42
|
+
import { auth, currentUser } from '@clerk/nextjs/server';
|
|
43
|
+
import { redirect } from 'next/navigation';
|
|
44
|
+
|
|
45
|
+
export default async function ProtectedPage() {
|
|
46
|
+
const { userId, sessionClaims } = auth();
|
|
47
|
+
|
|
48
|
+
if (!userId) {
|
|
49
|
+
redirect('/sign-in');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get full user object
|
|
53
|
+
const user = await currentUser();
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<h1>Welcome, {user?.firstName}</h1>
|
|
58
|
+
<p>Email: {user?.emailAddresses[0]?.emailAddress}</p>
|
|
59
|
+
<p>User ID: {userId}</p>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// API Route Protection
|
|
65
|
+
import { auth } from '@clerk/nextjs/server';
|
|
66
|
+
import { NextResponse } from 'next/server';
|
|
67
|
+
|
|
68
|
+
export async function GET() {
|
|
69
|
+
const { userId } = auth();
|
|
70
|
+
|
|
71
|
+
if (!userId) {
|
|
72
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Proceed with authenticated request
|
|
76
|
+
return NextResponse.json({ userId });
|
|
77
|
+
}
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## Client Components
|
|
81
|
+
\`\`\`tsx
|
|
82
|
+
'use client';
|
|
83
|
+
|
|
84
|
+
import {
|
|
85
|
+
SignIn,
|
|
86
|
+
SignUp,
|
|
87
|
+
SignOutButton,
|
|
88
|
+
SignedIn,
|
|
89
|
+
SignedOut,
|
|
90
|
+
UserButton,
|
|
91
|
+
useUser,
|
|
92
|
+
useAuth,
|
|
93
|
+
useClerk,
|
|
94
|
+
} from '@clerk/nextjs';
|
|
95
|
+
|
|
96
|
+
// Pre-built Components
|
|
97
|
+
export function AuthPage() {
|
|
98
|
+
return (
|
|
99
|
+
<div>
|
|
100
|
+
<SignedOut>
|
|
101
|
+
<SignIn routing="hash" />
|
|
102
|
+
{/* Or SignUp for registration */}
|
|
103
|
+
</SignedOut>
|
|
104
|
+
|
|
105
|
+
<SignedIn>
|
|
106
|
+
<UserButton afterSignOutUrl="/" />
|
|
107
|
+
</SignedIn>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Using hooks
|
|
113
|
+
export function UserProfile() {
|
|
114
|
+
const { isLoaded, isSignedIn, user } = useUser();
|
|
115
|
+
const { signOut, getToken } = useAuth();
|
|
116
|
+
const clerk = useClerk();
|
|
117
|
+
|
|
118
|
+
if (!isLoaded) return <div>Loading...</div>;
|
|
119
|
+
|
|
120
|
+
if (!isSignedIn) {
|
|
121
|
+
return <button onClick={() => clerk.openSignIn()}>Sign In</button>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const handleApiCall = async () => {
|
|
125
|
+
const token = await getToken();
|
|
126
|
+
// Use token for authenticated API calls
|
|
127
|
+
const response = await fetch('/api/protected', {
|
|
128
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div>
|
|
134
|
+
<img src={user.imageUrl} alt={user.fullName ?? ''} />
|
|
135
|
+
<h1>{user.fullName}</h1>
|
|
136
|
+
<p>{user.primaryEmailAddress?.emailAddress}</p>
|
|
137
|
+
|
|
138
|
+
<button onClick={handleApiCall}>Make API Call</button>
|
|
139
|
+
<SignOutButton>
|
|
140
|
+
<button>Sign Out</button>
|
|
141
|
+
</SignOutButton>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Custom Sign In Page
|
|
147
|
+
// app/sign-in/[[...sign-in]]/page.tsx
|
|
148
|
+
export default function SignInPage() {
|
|
149
|
+
return (
|
|
150
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
151
|
+
<SignIn
|
|
152
|
+
appearance={{
|
|
153
|
+
elements: {
|
|
154
|
+
rootBox: 'mx-auto',
|
|
155
|
+
card: 'shadow-xl',
|
|
156
|
+
},
|
|
157
|
+
}}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
\`\`\`
|
|
163
|
+
|
|
164
|
+
## Organizations & Roles
|
|
165
|
+
\`\`\`typescript
|
|
166
|
+
// Server-side organization check
|
|
167
|
+
import { auth } from '@clerk/nextjs/server';
|
|
168
|
+
|
|
169
|
+
export default async function OrgPage() {
|
|
170
|
+
const { orgId, orgRole, orgSlug } = auth();
|
|
171
|
+
|
|
172
|
+
if (!orgId) {
|
|
173
|
+
return <div>Please select an organization</div>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (orgRole !== 'org:admin') {
|
|
177
|
+
return <div>Admin access required</div>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return <div>Organization: {orgSlug}</div>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Client-side organization
|
|
184
|
+
'use client';
|
|
185
|
+
|
|
186
|
+
import {
|
|
187
|
+
OrganizationSwitcher,
|
|
188
|
+
OrganizationProfile,
|
|
189
|
+
useOrganization,
|
|
190
|
+
useOrganizationList,
|
|
191
|
+
} from '@clerk/nextjs';
|
|
192
|
+
|
|
193
|
+
export function OrgDashboard() {
|
|
194
|
+
const { organization, membership } = useOrganization();
|
|
195
|
+
const { userMemberships } = useOrganizationList();
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div>
|
|
199
|
+
<OrganizationSwitcher />
|
|
200
|
+
{organization && (
|
|
201
|
+
<div>
|
|
202
|
+
<h1>{organization.name}</h1>
|
|
203
|
+
<p>Your role: {membership?.role}</p>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
\`\`\`
|
|
210
|
+
|
|
211
|
+
## Webhooks
|
|
212
|
+
\`\`\`typescript
|
|
213
|
+
// app/api/webhooks/clerk/route.ts
|
|
214
|
+
import { Webhook } from 'svix';
|
|
215
|
+
import { headers } from 'next/headers';
|
|
216
|
+
import { WebhookEvent } from '@clerk/nextjs/server';
|
|
217
|
+
|
|
218
|
+
export async function POST(req: Request) {
|
|
219
|
+
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
|
|
220
|
+
|
|
221
|
+
if (!WEBHOOK_SECRET) {
|
|
222
|
+
throw new Error('Missing CLERK_WEBHOOK_SECRET');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const headerPayload = headers();
|
|
226
|
+
const svix_id = headerPayload.get('svix-id');
|
|
227
|
+
const svix_timestamp = headerPayload.get('svix-timestamp');
|
|
228
|
+
const svix_signature = headerPayload.get('svix-signature');
|
|
229
|
+
|
|
230
|
+
if (!svix_id || !svix_timestamp || !svix_signature) {
|
|
231
|
+
return new Response('Missing svix headers', { status: 400 });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const payload = await req.json();
|
|
235
|
+
const body = JSON.stringify(payload);
|
|
236
|
+
|
|
237
|
+
const wh = new Webhook(WEBHOOK_SECRET);
|
|
238
|
+
let evt: WebhookEvent;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
evt = wh.verify(body, {
|
|
242
|
+
'svix-id': svix_id,
|
|
243
|
+
'svix-timestamp': svix_timestamp,
|
|
244
|
+
'svix-signature': svix_signature,
|
|
245
|
+
}) as WebhookEvent;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return new Response('Invalid signature', { status: 400 });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const eventType = evt.type;
|
|
251
|
+
|
|
252
|
+
switch (eventType) {
|
|
253
|
+
case 'user.created':
|
|
254
|
+
const { id, email_addresses, first_name, last_name } = evt.data;
|
|
255
|
+
// Create user in your database
|
|
256
|
+
await db.user.create({
|
|
257
|
+
data: {
|
|
258
|
+
clerkId: id,
|
|
259
|
+
email: email_addresses[0]?.email_address,
|
|
260
|
+
name: \`\${first_name} \${last_name}\`,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case 'user.updated':
|
|
266
|
+
// Update user in your database
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
case 'user.deleted':
|
|
270
|
+
// Delete user from your database
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return new Response('OK', { status: 200 });
|
|
275
|
+
}
|
|
276
|
+
\`\`\`
|
|
277
|
+
|
|
278
|
+
## Custom Session Claims
|
|
279
|
+
\`\`\`typescript
|
|
280
|
+
// In Clerk Dashboard, add custom claims to session token
|
|
281
|
+
// Or use middleware to add dynamic claims
|
|
282
|
+
|
|
283
|
+
// middleware.ts
|
|
284
|
+
import { clerkMiddleware } from '@clerk/nextjs/server';
|
|
285
|
+
|
|
286
|
+
export default clerkMiddleware(async (auth, req) => {
|
|
287
|
+
const { userId, sessionClaims } = auth();
|
|
288
|
+
|
|
289
|
+
// Access custom claims
|
|
290
|
+
const role = sessionClaims?.metadata?.role as string;
|
|
291
|
+
|
|
292
|
+
if (req.nextUrl.pathname.startsWith('/admin') && role !== 'admin') {
|
|
293
|
+
return Response.redirect(new URL('/unauthorized', req.url));
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Access in components
|
|
298
|
+
const { sessionClaims } = auth();
|
|
299
|
+
const userRole = sessionClaims?.metadata?.role;
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
## Environment Variables
|
|
303
|
+
\`\`\`bash
|
|
304
|
+
# .env.local
|
|
305
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
|
|
306
|
+
CLERK_SECRET_KEY=sk_test_...
|
|
307
|
+
CLERK_WEBHOOK_SECRET=whsec_...
|
|
308
|
+
|
|
309
|
+
# Custom pages (optional)
|
|
310
|
+
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
|
311
|
+
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
|
312
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
|
|
313
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
|
|
314
|
+
\`\`\`
|
|
315
|
+
|
|
316
|
+
## ❌ DON'T
|
|
317
|
+
- Expose CLERK_SECRET_KEY to client
|
|
318
|
+
- Skip webhook signature verification
|
|
319
|
+
- Forget middleware for protected routes
|
|
320
|
+
- Store sensitive data in public metadata
|
|
321
|
+
|
|
322
|
+
## ✅ DO
|
|
323
|
+
- Use clerkMiddleware for route protection
|
|
324
|
+
- Sync user data via webhooks
|
|
325
|
+
- Use organizations for multi-tenant apps
|
|
326
|
+
- Use session claims for role-based access
|
|
327
|
+
- Customize appearance to match your brand
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# Firebase Auth Skill
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
// lib/firebase.ts
|
|
6
|
+
import { initializeApp, getApps } from 'firebase/app';
|
|
7
|
+
import { getAuth } from 'firebase/auth';
|
|
8
|
+
import { getFirestore } from 'firebase/firestore';
|
|
9
|
+
|
|
10
|
+
const firebaseConfig = {
|
|
11
|
+
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
|
12
|
+
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
|
13
|
+
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
|
14
|
+
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
|
15
|
+
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
|
16
|
+
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Initialize Firebase (prevent multiple instances)
|
|
20
|
+
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
|
|
21
|
+
|
|
22
|
+
export const auth = getAuth(app);
|
|
23
|
+
export const db = getFirestore(app);
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
## Authentication Methods
|
|
27
|
+
\`\`\`typescript
|
|
28
|
+
import {
|
|
29
|
+
createUserWithEmailAndPassword,
|
|
30
|
+
signInWithEmailAndPassword,
|
|
31
|
+
signInWithPopup,
|
|
32
|
+
signInWithRedirect,
|
|
33
|
+
GoogleAuthProvider,
|
|
34
|
+
GithubAuthProvider,
|
|
35
|
+
sendPasswordResetEmail,
|
|
36
|
+
sendEmailVerification,
|
|
37
|
+
updateProfile,
|
|
38
|
+
signOut,
|
|
39
|
+
} from 'firebase/auth';
|
|
40
|
+
import { auth } from '@/lib/firebase';
|
|
41
|
+
|
|
42
|
+
// Email/Password Sign Up
|
|
43
|
+
async function signUp(email: string, password: string, displayName: string) {
|
|
44
|
+
const { user } = await createUserWithEmailAndPassword(auth, email, password);
|
|
45
|
+
|
|
46
|
+
// Update profile with display name
|
|
47
|
+
await updateProfile(user, { displayName });
|
|
48
|
+
|
|
49
|
+
// Send verification email
|
|
50
|
+
await sendEmailVerification(user);
|
|
51
|
+
|
|
52
|
+
return user;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Email/Password Sign In
|
|
56
|
+
async function signIn(email: string, password: string) {
|
|
57
|
+
const { user } = await signInWithEmailAndPassword(auth, email, password);
|
|
58
|
+
return user;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Google Sign In (Popup)
|
|
62
|
+
async function signInWithGoogle() {
|
|
63
|
+
const provider = new GoogleAuthProvider();
|
|
64
|
+
provider.addScope('profile');
|
|
65
|
+
provider.addScope('email');
|
|
66
|
+
|
|
67
|
+
const { user } = await signInWithPopup(auth, provider);
|
|
68
|
+
return user;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Google Sign In (Redirect - better for mobile)
|
|
72
|
+
async function signInWithGoogleRedirect() {
|
|
73
|
+
const provider = new GoogleAuthProvider();
|
|
74
|
+
await signInWithRedirect(auth, provider);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// GitHub Sign In
|
|
78
|
+
async function signInWithGitHub() {
|
|
79
|
+
const provider = new GithubAuthProvider();
|
|
80
|
+
provider.addScope('read:user');
|
|
81
|
+
|
|
82
|
+
const { user } = await signInWithPopup(auth, provider);
|
|
83
|
+
return user;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Password Reset
|
|
87
|
+
async function resetPassword(email: string) {
|
|
88
|
+
await sendPasswordResetEmail(auth, email, {
|
|
89
|
+
url: \`\${window.location.origin}/login\`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Sign Out
|
|
94
|
+
async function logout() {
|
|
95
|
+
await signOut(auth);
|
|
96
|
+
}
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
## Auth State Listener
|
|
100
|
+
\`\`\`typescript
|
|
101
|
+
import { onAuthStateChanged, User } from 'firebase/auth';
|
|
102
|
+
import { auth } from '@/lib/firebase';
|
|
103
|
+
|
|
104
|
+
// React Hook
|
|
105
|
+
import { useState, useEffect, createContext, useContext } from 'react';
|
|
106
|
+
|
|
107
|
+
interface AuthContextType {
|
|
108
|
+
user: User | null;
|
|
109
|
+
loading: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const AuthContext = createContext<AuthContextType>({ user: null, loading: true });
|
|
113
|
+
|
|
114
|
+
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
115
|
+
const [user, setUser] = useState<User | null>(null);
|
|
116
|
+
const [loading, setLoading] = useState(true);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
|
120
|
+
setUser(user);
|
|
121
|
+
setLoading(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return () => unsubscribe();
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<AuthContext.Provider value={{ user, loading }}>
|
|
129
|
+
{children}
|
|
130
|
+
</AuthContext.Provider>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function useAuth() {
|
|
135
|
+
const context = useContext(AuthContext);
|
|
136
|
+
if (!context) {
|
|
137
|
+
throw new Error('useAuth must be used within AuthProvider');
|
|
138
|
+
}
|
|
139
|
+
return context;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Usage
|
|
143
|
+
function ProtectedComponent() {
|
|
144
|
+
const { user, loading } = useAuth();
|
|
145
|
+
|
|
146
|
+
if (loading) return <div>Loading...</div>;
|
|
147
|
+
if (!user) return <div>Please sign in</div>;
|
|
148
|
+
|
|
149
|
+
return <div>Welcome, {user.displayName}</div>;
|
|
150
|
+
}
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
## Protected Routes
|
|
154
|
+
\`\`\`tsx
|
|
155
|
+
'use client';
|
|
156
|
+
|
|
157
|
+
import { useAuth } from '@/hooks/useAuth';
|
|
158
|
+
import { useRouter } from 'next/navigation';
|
|
159
|
+
import { useEffect } from 'react';
|
|
160
|
+
|
|
161
|
+
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
162
|
+
const { user, loading } = useAuth();
|
|
163
|
+
const router = useRouter();
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!loading && !user) {
|
|
167
|
+
router.push('/login');
|
|
168
|
+
}
|
|
169
|
+
}, [user, loading, router]);
|
|
170
|
+
|
|
171
|
+
if (loading) {
|
|
172
|
+
return <div>Loading...</div>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!user) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return <>{children}</>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Usage in page
|
|
183
|
+
export default function DashboardPage() {
|
|
184
|
+
return (
|
|
185
|
+
<ProtectedRoute>
|
|
186
|
+
<Dashboard />
|
|
187
|
+
</ProtectedRoute>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
\`\`\`
|
|
191
|
+
|
|
192
|
+
## ID Tokens for API Auth
|
|
193
|
+
\`\`\`typescript
|
|
194
|
+
// Client: Get ID token for API calls
|
|
195
|
+
async function getIdToken() {
|
|
196
|
+
const user = auth.currentUser;
|
|
197
|
+
if (!user) throw new Error('Not authenticated');
|
|
198
|
+
|
|
199
|
+
const token = await user.getIdToken();
|
|
200
|
+
return token;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// API call with token
|
|
204
|
+
async function fetchProtectedData() {
|
|
205
|
+
const token = await getIdToken();
|
|
206
|
+
|
|
207
|
+
const response = await fetch('/api/protected', {
|
|
208
|
+
headers: {
|
|
209
|
+
Authorization: \`Bearer \${token}\`,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return response.json();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Server: Verify ID token (API route)
|
|
217
|
+
// app/api/protected/route.ts
|
|
218
|
+
import { getAuth } from 'firebase-admin/auth';
|
|
219
|
+
import { initializeApp, getApps, cert } from 'firebase-admin/app';
|
|
220
|
+
|
|
221
|
+
// Initialize Firebase Admin
|
|
222
|
+
if (getApps().length === 0) {
|
|
223
|
+
initializeApp({
|
|
224
|
+
credential: cert({
|
|
225
|
+
projectId: process.env.FIREBASE_PROJECT_ID,
|
|
226
|
+
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
|
227
|
+
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\\\n/g, '\\n'),
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function GET(request: Request) {
|
|
233
|
+
const authHeader = request.headers.get('Authorization');
|
|
234
|
+
|
|
235
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
236
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const token = authHeader.split('Bearer ')[1];
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const decodedToken = await getAuth().verifyIdToken(token);
|
|
243
|
+
const userId = decodedToken.uid;
|
|
244
|
+
|
|
245
|
+
// Proceed with authenticated request
|
|
246
|
+
return Response.json({ userId });
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return Response.json({ error: 'Invalid token' }, { status: 401 });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
\`\`\`
|
|
252
|
+
|
|
253
|
+
## Custom Claims (Roles)
|
|
254
|
+
\`\`\`typescript
|
|
255
|
+
// Server-side: Set custom claims (Firebase Admin)
|
|
256
|
+
import { getAuth } from 'firebase-admin/auth';
|
|
257
|
+
|
|
258
|
+
async function setUserRole(uid: string, role: string) {
|
|
259
|
+
await getAuth().setCustomUserClaims(uid, { role });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Client-side: Check claims
|
|
263
|
+
async function checkAdmin() {
|
|
264
|
+
const user = auth.currentUser;
|
|
265
|
+
if (!user) return false;
|
|
266
|
+
|
|
267
|
+
const tokenResult = await user.getIdTokenResult();
|
|
268
|
+
return tokenResult.claims.role === 'admin';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Force token refresh after claims change
|
|
272
|
+
await auth.currentUser?.getIdToken(true);
|
|
273
|
+
\`\`\`
|
|
274
|
+
|
|
275
|
+
## Firestore Security Rules
|
|
276
|
+
\`\`\`javascript
|
|
277
|
+
// firestore.rules
|
|
278
|
+
rules_version = '2';
|
|
279
|
+
service cloud.firestore {
|
|
280
|
+
match /databases/{database}/documents {
|
|
281
|
+
// Users can only access their own data
|
|
282
|
+
match /users/{userId} {
|
|
283
|
+
allow read, write: if request.auth != null && request.auth.uid == userId;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Posts are readable by all, writable by author
|
|
287
|
+
match /posts/{postId} {
|
|
288
|
+
allow read: if true;
|
|
289
|
+
allow create: if request.auth != null;
|
|
290
|
+
allow update, delete: if request.auth != null
|
|
291
|
+
&& request.auth.uid == resource.data.authorId;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Admin-only access
|
|
295
|
+
match /admin/{document=**} {
|
|
296
|
+
allow read, write: if request.auth != null
|
|
297
|
+
&& request.auth.token.role == 'admin';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
\`\`\`
|
|
302
|
+
|
|
303
|
+
## Phone Authentication
|
|
304
|
+
\`\`\`typescript
|
|
305
|
+
import {
|
|
306
|
+
RecaptchaVerifier,
|
|
307
|
+
signInWithPhoneNumber,
|
|
308
|
+
ConfirmationResult,
|
|
309
|
+
} from 'firebase/auth';
|
|
310
|
+
|
|
311
|
+
let confirmationResult: ConfirmationResult;
|
|
312
|
+
|
|
313
|
+
// Setup reCAPTCHA
|
|
314
|
+
function setupRecaptcha() {
|
|
315
|
+
window.recaptchaVerifier = new RecaptchaVerifier(auth, 'recaptcha-container', {
|
|
316
|
+
size: 'invisible',
|
|
317
|
+
callback: () => {
|
|
318
|
+
// reCAPTCHA solved
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Send OTP
|
|
324
|
+
async function sendOTP(phoneNumber: string) {
|
|
325
|
+
const appVerifier = window.recaptchaVerifier;
|
|
326
|
+
confirmationResult = await signInWithPhoneNumber(auth, phoneNumber, appVerifier);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Verify OTP
|
|
330
|
+
async function verifyOTP(code: string) {
|
|
331
|
+
const result = await confirmationResult.confirm(code);
|
|
332
|
+
return result.user;
|
|
333
|
+
}
|
|
334
|
+
\`\`\`
|
|
335
|
+
|
|
336
|
+
## User Profile Updates
|
|
337
|
+
\`\`\`typescript
|
|
338
|
+
import { updateProfile, updateEmail, updatePassword, User } from 'firebase/auth';
|
|
339
|
+
|
|
340
|
+
async function updateUserProfile(user: User, data: { displayName?: string; photoURL?: string }) {
|
|
341
|
+
await updateProfile(user, data);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function updateUserEmail(user: User, newEmail: string) {
|
|
345
|
+
await updateEmail(user, newEmail);
|
|
346
|
+
// User needs to re-verify email
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function updateUserPassword(user: User, newPassword: string) {
|
|
350
|
+
await updatePassword(user, newPassword);
|
|
351
|
+
}
|
|
352
|
+
\`\`\`
|
|
353
|
+
|
|
354
|
+
## ❌ DON'T
|
|
355
|
+
- Store Firebase config with private keys on client
|
|
356
|
+
- Skip email verification for sensitive apps
|
|
357
|
+
- Use currentUser without waiting for auth state
|
|
358
|
+
- Expose Firebase Admin credentials to client
|
|
359
|
+
- Skip Firestore security rules
|
|
360
|
+
|
|
361
|
+
## ✅ DO
|
|
362
|
+
- Use onAuthStateChanged for auth state
|
|
363
|
+
- Verify ID tokens on server for API calls
|
|
364
|
+
- Use custom claims for role-based access
|
|
365
|
+
- Implement proper Firestore security rules
|
|
366
|
+
- Handle all auth errors gracefully
|
|
367
|
+
- Force token refresh after claims change
|