autoworkflow 3.1.5 → 3.6.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/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -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 +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# NextAuth.js Skill
|
|
2
|
+
|
|
3
|
+
## Setup (App Router)
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
// app/api/auth/[...nextauth]/route.ts
|
|
6
|
+
import NextAuth from 'next-auth';
|
|
7
|
+
import { authOptions } from '@/lib/auth';
|
|
8
|
+
|
|
9
|
+
const handler = NextAuth(authOptions);
|
|
10
|
+
export { handler as GET, handler as POST };
|
|
11
|
+
|
|
12
|
+
// lib/auth.ts
|
|
13
|
+
import { NextAuthOptions } from 'next-auth';
|
|
14
|
+
import GoogleProvider from 'next-auth/providers/google';
|
|
15
|
+
import GitHubProvider from 'next-auth/providers/github';
|
|
16
|
+
import CredentialsProvider from 'next-auth/providers/credentials';
|
|
17
|
+
import { PrismaAdapter } from '@auth/prisma-adapter';
|
|
18
|
+
import { prisma } from '@/lib/prisma';
|
|
19
|
+
import bcrypt from 'bcryptjs';
|
|
20
|
+
|
|
21
|
+
export const authOptions: NextAuthOptions = {
|
|
22
|
+
adapter: PrismaAdapter(prisma),
|
|
23
|
+
|
|
24
|
+
providers: [
|
|
25
|
+
// OAuth Providers
|
|
26
|
+
GoogleProvider({
|
|
27
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
28
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
GitHubProvider({
|
|
32
|
+
clientId: process.env.GITHUB_ID!,
|
|
33
|
+
clientSecret: process.env.GITHUB_SECRET!,
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
// Credentials Provider (email/password)
|
|
37
|
+
CredentialsProvider({
|
|
38
|
+
name: 'credentials',
|
|
39
|
+
credentials: {
|
|
40
|
+
email: { label: 'Email', type: 'email' },
|
|
41
|
+
password: { label: 'Password', type: 'password' },
|
|
42
|
+
},
|
|
43
|
+
async authorize(credentials) {
|
|
44
|
+
if (!credentials?.email || !credentials?.password) {
|
|
45
|
+
throw new Error('Invalid credentials');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const user = await prisma.user.findUnique({
|
|
49
|
+
where: { email: credentials.email },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!user || !user.password) {
|
|
53
|
+
throw new Error('Invalid credentials');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isValid = await bcrypt.compare(credentials.password, user.password);
|
|
57
|
+
|
|
58
|
+
if (!isValid) {
|
|
59
|
+
throw new Error('Invalid credentials');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
id: user.id,
|
|
64
|
+
email: user.email,
|
|
65
|
+
name: user.name,
|
|
66
|
+
image: user.image,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
|
|
72
|
+
session: {
|
|
73
|
+
strategy: 'jwt', // Required for credentials provider
|
|
74
|
+
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
pages: {
|
|
78
|
+
signIn: '/auth/signin',
|
|
79
|
+
signOut: '/auth/signout',
|
|
80
|
+
error: '/auth/error',
|
|
81
|
+
verifyRequest: '/auth/verify-request',
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
callbacks: {
|
|
85
|
+
async jwt({ token, user, account }) {
|
|
86
|
+
// First sign in
|
|
87
|
+
if (user) {
|
|
88
|
+
token.id = user.id;
|
|
89
|
+
token.role = user.role;
|
|
90
|
+
}
|
|
91
|
+
if (account) {
|
|
92
|
+
token.accessToken = account.access_token;
|
|
93
|
+
}
|
|
94
|
+
return token;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async session({ session, token }) {
|
|
98
|
+
if (session.user) {
|
|
99
|
+
session.user.id = token.id as string;
|
|
100
|
+
session.user.role = token.role as string;
|
|
101
|
+
}
|
|
102
|
+
return session;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async signIn({ user, account, profile }) {
|
|
106
|
+
// Custom sign-in logic
|
|
107
|
+
if (account?.provider === 'google') {
|
|
108
|
+
return profile?.email?.endsWith('@company.com') ?? false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async redirect({ url, baseUrl }) {
|
|
114
|
+
// Custom redirect logic
|
|
115
|
+
if (url.startsWith(baseUrl)) return url;
|
|
116
|
+
if (url.startsWith('/')) return \`\${baseUrl}\${url}\`;
|
|
117
|
+
return baseUrl;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
events: {
|
|
122
|
+
async signIn({ user, account, isNewUser }) {
|
|
123
|
+
if (isNewUser) {
|
|
124
|
+
// Send welcome email, create default settings, etc.
|
|
125
|
+
console.log('New user signed up:', user.email);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async signOut({ token }) {
|
|
129
|
+
// Cleanup on sign out
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
debug: process.env.NODE_ENV === 'development',
|
|
134
|
+
};
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
## TypeScript Types
|
|
138
|
+
\`\`\`typescript
|
|
139
|
+
// types/next-auth.d.ts
|
|
140
|
+
import { DefaultSession, DefaultUser } from 'next-auth';
|
|
141
|
+
import { JWT, DefaultJWT } from 'next-auth/jwt';
|
|
142
|
+
|
|
143
|
+
declare module 'next-auth' {
|
|
144
|
+
interface Session {
|
|
145
|
+
user: {
|
|
146
|
+
id: string;
|
|
147
|
+
role: string;
|
|
148
|
+
} & DefaultSession['user'];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface User extends DefaultUser {
|
|
152
|
+
role: string;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
declare module 'next-auth/jwt' {
|
|
157
|
+
interface JWT extends DefaultJWT {
|
|
158
|
+
id: string;
|
|
159
|
+
role: string;
|
|
160
|
+
accessToken?: string;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
\`\`\`
|
|
164
|
+
|
|
165
|
+
## Server Components (App Router)
|
|
166
|
+
\`\`\`typescript
|
|
167
|
+
// Get session in Server Component
|
|
168
|
+
import { getServerSession } from 'next-auth';
|
|
169
|
+
import { authOptions } from '@/lib/auth';
|
|
170
|
+
import { redirect } from 'next/navigation';
|
|
171
|
+
|
|
172
|
+
export default async function ProtectedPage() {
|
|
173
|
+
const session = await getServerSession(authOptions);
|
|
174
|
+
|
|
175
|
+
if (!session) {
|
|
176
|
+
redirect('/auth/signin');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div>
|
|
181
|
+
<h1>Welcome, {session.user.name}</h1>
|
|
182
|
+
<p>User ID: {session.user.id}</p>
|
|
183
|
+
<p>Role: {session.user.role}</p>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// API Route Protection
|
|
189
|
+
import { getServerSession } from 'next-auth';
|
|
190
|
+
import { authOptions } from '@/lib/auth';
|
|
191
|
+
import { NextResponse } from 'next/server';
|
|
192
|
+
|
|
193
|
+
export async function GET() {
|
|
194
|
+
const session = await getServerSession(authOptions);
|
|
195
|
+
|
|
196
|
+
if (!session) {
|
|
197
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Proceed with authenticated request
|
|
201
|
+
return NextResponse.json({ user: session.user });
|
|
202
|
+
}
|
|
203
|
+
\`\`\`
|
|
204
|
+
|
|
205
|
+
## Client Components
|
|
206
|
+
\`\`\`tsx
|
|
207
|
+
'use client';
|
|
208
|
+
|
|
209
|
+
import { useSession, signIn, signOut } from 'next-auth/react';
|
|
210
|
+
|
|
211
|
+
export function AuthButton() {
|
|
212
|
+
const { data: session, status } = useSession();
|
|
213
|
+
|
|
214
|
+
if (status === 'loading') {
|
|
215
|
+
return <div>Loading...</div>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (session) {
|
|
219
|
+
return (
|
|
220
|
+
<div>
|
|
221
|
+
<p>Signed in as {session.user?.email}</p>
|
|
222
|
+
<button onClick={() => signOut({ callbackUrl: '/' })}>
|
|
223
|
+
Sign out
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div>
|
|
231
|
+
<button onClick={() => signIn('google')}>Sign in with Google</button>
|
|
232
|
+
<button onClick={() => signIn('github')}>Sign in with GitHub</button>
|
|
233
|
+
<button onClick={() => signIn('credentials', { email: '', password: '' })}>
|
|
234
|
+
Sign in with Email
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Session Provider (required in layout)
|
|
241
|
+
// app/providers.tsx
|
|
242
|
+
'use client';
|
|
243
|
+
|
|
244
|
+
import { SessionProvider } from 'next-auth/react';
|
|
245
|
+
|
|
246
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
247
|
+
return <SessionProvider>{children}</SessionProvider>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// app/layout.tsx
|
|
251
|
+
import { Providers } from './providers';
|
|
252
|
+
|
|
253
|
+
export default function RootLayout({ children }) {
|
|
254
|
+
return (
|
|
255
|
+
<html>
|
|
256
|
+
<body>
|
|
257
|
+
<Providers>{children}</Providers>
|
|
258
|
+
</body>
|
|
259
|
+
</html>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
## Middleware (Route Protection)
|
|
265
|
+
\`\`\`typescript
|
|
266
|
+
// middleware.ts
|
|
267
|
+
import { withAuth } from 'next-auth/middleware';
|
|
268
|
+
import { NextResponse } from 'next/server';
|
|
269
|
+
|
|
270
|
+
export default withAuth(
|
|
271
|
+
function middleware(req) {
|
|
272
|
+
const token = req.nextauth.token;
|
|
273
|
+
const path = req.nextUrl.pathname;
|
|
274
|
+
|
|
275
|
+
// Admin-only routes
|
|
276
|
+
if (path.startsWith('/admin') && token?.role !== 'admin') {
|
|
277
|
+
return NextResponse.redirect(new URL('/unauthorized', req.url));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return NextResponse.next();
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
callbacks: {
|
|
284
|
+
authorized: ({ token }) => !!token,
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
export const config = {
|
|
290
|
+
matcher: ['/dashboard/:path*', '/admin/:path*', '/api/protected/:path*'],
|
|
291
|
+
};
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
## Prisma Schema
|
|
295
|
+
\`\`\`prisma
|
|
296
|
+
// prisma/schema.prisma
|
|
297
|
+
model User {
|
|
298
|
+
id String @id @default(cuid())
|
|
299
|
+
name String?
|
|
300
|
+
email String? @unique
|
|
301
|
+
emailVerified DateTime?
|
|
302
|
+
image String?
|
|
303
|
+
password String? // For credentials provider
|
|
304
|
+
role String @default("user")
|
|
305
|
+
accounts Account[]
|
|
306
|
+
sessions Session[]
|
|
307
|
+
createdAt DateTime @default(now())
|
|
308
|
+
updatedAt DateTime @updatedAt
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
model Account {
|
|
312
|
+
id String @id @default(cuid())
|
|
313
|
+
userId String
|
|
314
|
+
type String
|
|
315
|
+
provider String
|
|
316
|
+
providerAccountId String
|
|
317
|
+
refresh_token String? @db.Text
|
|
318
|
+
access_token String? @db.Text
|
|
319
|
+
expires_at Int?
|
|
320
|
+
token_type String?
|
|
321
|
+
scope String?
|
|
322
|
+
id_token String? @db.Text
|
|
323
|
+
session_state String?
|
|
324
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
325
|
+
|
|
326
|
+
@@unique([provider, providerAccountId])
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
model Session {
|
|
330
|
+
id String @id @default(cuid())
|
|
331
|
+
sessionToken String @unique
|
|
332
|
+
userId String
|
|
333
|
+
expires DateTime
|
|
334
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
model VerificationToken {
|
|
338
|
+
identifier String
|
|
339
|
+
token String @unique
|
|
340
|
+
expires DateTime
|
|
341
|
+
|
|
342
|
+
@@unique([identifier, token])
|
|
343
|
+
}
|
|
344
|
+
\`\`\`
|
|
345
|
+
|
|
346
|
+
## ❌ DON'T
|
|
347
|
+
- Store sensitive data in JWT (it's readable client-side)
|
|
348
|
+
- Use credentials provider without HTTPS
|
|
349
|
+
- Forget to add NEXTAUTH_SECRET to env
|
|
350
|
+
- Skip session validation in API routes
|
|
351
|
+
- Expose internal user IDs unnecessarily
|
|
352
|
+
|
|
353
|
+
## ✅ DO
|
|
354
|
+
- Use environment variables for secrets
|
|
355
|
+
- Implement proper session callbacks
|
|
356
|
+
- Add TypeScript declarations for extended types
|
|
357
|
+
- Use middleware for route protection
|
|
358
|
+
- Validate roles in callbacks and middleware
|
|
359
|
+
- Use database adapter for persistent sessions
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Supabase Auth Skill
|
|
2
|
+
|
|
3
|
+
## Setup (Next.js App Router)
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
// lib/supabase/client.ts (Browser client)
|
|
6
|
+
import { createBrowserClient } from '@supabase/ssr';
|
|
7
|
+
|
|
8
|
+
export function createClient() {
|
|
9
|
+
return createBrowserClient(
|
|
10
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
11
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// lib/supabase/server.ts (Server client)
|
|
16
|
+
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
|
17
|
+
import { cookies } from 'next/headers';
|
|
18
|
+
|
|
19
|
+
export function createClient() {
|
|
20
|
+
const cookieStore = cookies();
|
|
21
|
+
|
|
22
|
+
return createServerClient(
|
|
23
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
24
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
25
|
+
{
|
|
26
|
+
cookies: {
|
|
27
|
+
get(name: string) {
|
|
28
|
+
return cookieStore.get(name)?.value;
|
|
29
|
+
},
|
|
30
|
+
set(name: string, value: string, options: CookieOptions) {
|
|
31
|
+
cookieStore.set({ name, value, ...options });
|
|
32
|
+
},
|
|
33
|
+
remove(name: string, options: CookieOptions) {
|
|
34
|
+
cookieStore.set({ name, value: '', ...options });
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// middleware.ts
|
|
42
|
+
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
|
43
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
44
|
+
|
|
45
|
+
export async function middleware(request: NextRequest) {
|
|
46
|
+
let response = NextResponse.next({ request: { headers: request.headers } });
|
|
47
|
+
|
|
48
|
+
const supabase = createServerClient(
|
|
49
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
50
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
51
|
+
{
|
|
52
|
+
cookies: {
|
|
53
|
+
get(name: string) {
|
|
54
|
+
return request.cookies.get(name)?.value;
|
|
55
|
+
},
|
|
56
|
+
set(name: string, value: string, options: CookieOptions) {
|
|
57
|
+
response.cookies.set({ name, value, ...options });
|
|
58
|
+
},
|
|
59
|
+
remove(name: string, options: CookieOptions) {
|
|
60
|
+
response.cookies.set({ name, value: '', ...options });
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Refresh session if needed
|
|
67
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
68
|
+
|
|
69
|
+
// Protect routes
|
|
70
|
+
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
71
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const config = {
|
|
78
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
79
|
+
};
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
## Authentication Methods
|
|
83
|
+
\`\`\`typescript
|
|
84
|
+
const supabase = createClient();
|
|
85
|
+
|
|
86
|
+
// Email/Password Sign Up
|
|
87
|
+
const { data, error } = await supabase.auth.signUp({
|
|
88
|
+
email: 'user@example.com',
|
|
89
|
+
password: 'password123',
|
|
90
|
+
options: {
|
|
91
|
+
data: {
|
|
92
|
+
full_name: 'John Doe',
|
|
93
|
+
avatar_url: 'https://example.com/avatar.png',
|
|
94
|
+
},
|
|
95
|
+
emailRedirectTo: \`\${window.location.origin}/auth/callback\`,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Email/Password Sign In
|
|
100
|
+
const { data, error } = await supabase.auth.signInWithPassword({
|
|
101
|
+
email: 'user@example.com',
|
|
102
|
+
password: 'password123',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// OAuth Sign In
|
|
106
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
107
|
+
provider: 'google', // or 'github', 'discord', etc.
|
|
108
|
+
options: {
|
|
109
|
+
redirectTo: \`\${window.location.origin}/auth/callback\`,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Magic Link
|
|
114
|
+
const { data, error } = await supabase.auth.signInWithOtp({
|
|
115
|
+
email: 'user@example.com',
|
|
116
|
+
options: {
|
|
117
|
+
emailRedirectTo: \`\${window.location.origin}/auth/callback\`,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Phone/SMS OTP
|
|
122
|
+
const { data, error } = await supabase.auth.signInWithOtp({
|
|
123
|
+
phone: '+15551234567',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Verify OTP
|
|
127
|
+
const { data, error } = await supabase.auth.verifyOtp({
|
|
128
|
+
phone: '+15551234567',
|
|
129
|
+
token: '123456',
|
|
130
|
+
type: 'sms',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Sign Out
|
|
134
|
+
await supabase.auth.signOut();
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
## Auth Callback Handler
|
|
138
|
+
\`\`\`typescript
|
|
139
|
+
// app/auth/callback/route.ts
|
|
140
|
+
import { createClient } from '@/lib/supabase/server';
|
|
141
|
+
import { NextResponse } from 'next/server';
|
|
142
|
+
|
|
143
|
+
export async function GET(request: Request) {
|
|
144
|
+
const requestUrl = new URL(request.url);
|
|
145
|
+
const code = requestUrl.searchParams.get('code');
|
|
146
|
+
|
|
147
|
+
if (code) {
|
|
148
|
+
const supabase = createClient();
|
|
149
|
+
await supabase.auth.exchangeCodeForSession(code);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return NextResponse.redirect(new URL('/dashboard', request.url));
|
|
153
|
+
}
|
|
154
|
+
\`\`\`
|
|
155
|
+
|
|
156
|
+
## Server Components
|
|
157
|
+
\`\`\`typescript
|
|
158
|
+
// app/dashboard/page.tsx
|
|
159
|
+
import { createClient } from '@/lib/supabase/server';
|
|
160
|
+
import { redirect } from 'next/navigation';
|
|
161
|
+
|
|
162
|
+
export default async function DashboardPage() {
|
|
163
|
+
const supabase = createClient();
|
|
164
|
+
|
|
165
|
+
const { data: { user }, error } = await supabase.auth.getUser();
|
|
166
|
+
|
|
167
|
+
if (!user) {
|
|
168
|
+
redirect('/login');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Fetch user's data
|
|
172
|
+
const { data: profile } = await supabase
|
|
173
|
+
.from('profiles')
|
|
174
|
+
.select('*')
|
|
175
|
+
.eq('id', user.id)
|
|
176
|
+
.single();
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div>
|
|
180
|
+
<h1>Welcome, {profile?.full_name || user.email}</h1>
|
|
181
|
+
<p>Email: {user.email}</p>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
\`\`\`
|
|
186
|
+
|
|
187
|
+
## Client Components
|
|
188
|
+
\`\`\`tsx
|
|
189
|
+
'use client';
|
|
190
|
+
|
|
191
|
+
import { createClient } from '@/lib/supabase/client';
|
|
192
|
+
import { useEffect, useState } from 'react';
|
|
193
|
+
import { User, Session } from '@supabase/supabase-js';
|
|
194
|
+
|
|
195
|
+
export function useAuth() {
|
|
196
|
+
const supabase = createClient();
|
|
197
|
+
const [user, setUser] = useState<User | null>(null);
|
|
198
|
+
const [loading, setLoading] = useState(true);
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
// Get initial session
|
|
202
|
+
supabase.auth.getSession().then(({ data: { session } }) => {
|
|
203
|
+
setUser(session?.user ?? null);
|
|
204
|
+
setLoading(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Listen for auth changes
|
|
208
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
|
209
|
+
(event, session) => {
|
|
210
|
+
setUser(session?.user ?? null);
|
|
211
|
+
|
|
212
|
+
if (event === 'SIGNED_IN') {
|
|
213
|
+
// Handle sign in
|
|
214
|
+
}
|
|
215
|
+
if (event === 'SIGNED_OUT') {
|
|
216
|
+
// Handle sign out
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return () => subscription.unsubscribe();
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
return { user, loading };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Login Form Component
|
|
228
|
+
export function LoginForm() {
|
|
229
|
+
const supabase = createClient();
|
|
230
|
+
const [email, setEmail] = useState('');
|
|
231
|
+
const [password, setPassword] = useState('');
|
|
232
|
+
const [loading, setLoading] = useState(false);
|
|
233
|
+
const [error, setError] = useState<string | null>(null);
|
|
234
|
+
|
|
235
|
+
const handleLogin = async (e: React.FormEvent) => {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
setLoading(true);
|
|
238
|
+
setError(null);
|
|
239
|
+
|
|
240
|
+
const { error } = await supabase.auth.signInWithPassword({
|
|
241
|
+
email,
|
|
242
|
+
password,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (error) {
|
|
246
|
+
setError(error.message);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setLoading(false);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<form onSubmit={handleLogin}>
|
|
254
|
+
<input
|
|
255
|
+
type="email"
|
|
256
|
+
value={email}
|
|
257
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
258
|
+
placeholder="Email"
|
|
259
|
+
required
|
|
260
|
+
/>
|
|
261
|
+
<input
|
|
262
|
+
type="password"
|
|
263
|
+
value={password}
|
|
264
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
265
|
+
placeholder="Password"
|
|
266
|
+
required
|
|
267
|
+
/>
|
|
268
|
+
{error && <p className="error">{error}</p>}
|
|
269
|
+
<button type="submit" disabled={loading}>
|
|
270
|
+
{loading ? 'Loading...' : 'Sign In'}
|
|
271
|
+
</button>
|
|
272
|
+
</form>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
\`\`\`
|
|
276
|
+
|
|
277
|
+
## Row Level Security (RLS)
|
|
278
|
+
\`\`\`sql
|
|
279
|
+
-- Enable RLS on tables
|
|
280
|
+
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
|
281
|
+
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
|
|
282
|
+
|
|
283
|
+
-- Users can only read their own profile
|
|
284
|
+
CREATE POLICY "Users can view own profile"
|
|
285
|
+
ON profiles FOR SELECT
|
|
286
|
+
USING (auth.uid() = id);
|
|
287
|
+
|
|
288
|
+
-- Users can update their own profile
|
|
289
|
+
CREATE POLICY "Users can update own profile"
|
|
290
|
+
ON profiles FOR UPDATE
|
|
291
|
+
USING (auth.uid() = id);
|
|
292
|
+
|
|
293
|
+
-- Users can read all published posts
|
|
294
|
+
CREATE POLICY "Anyone can view published posts"
|
|
295
|
+
ON posts FOR SELECT
|
|
296
|
+
USING (published = true);
|
|
297
|
+
|
|
298
|
+
-- Users can CRUD their own posts
|
|
299
|
+
CREATE POLICY "Users can manage own posts"
|
|
300
|
+
ON posts FOR ALL
|
|
301
|
+
USING (auth.uid() = author_id);
|
|
302
|
+
|
|
303
|
+
-- Insert policy for new users
|
|
304
|
+
CREATE POLICY "Users can create profile"
|
|
305
|
+
ON profiles FOR INSERT
|
|
306
|
+
WITH CHECK (auth.uid() = id);
|
|
307
|
+
\`\`\`
|
|
308
|
+
|
|
309
|
+
## Database Triggers for Auth
|
|
310
|
+
\`\`\`sql
|
|
311
|
+
-- Auto-create profile on user signup
|
|
312
|
+
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
|
313
|
+
RETURNS TRIGGER AS $$
|
|
314
|
+
BEGIN
|
|
315
|
+
INSERT INTO public.profiles (id, email, full_name, avatar_url)
|
|
316
|
+
VALUES (
|
|
317
|
+
new.id,
|
|
318
|
+
new.email,
|
|
319
|
+
new.raw_user_meta_data->>'full_name',
|
|
320
|
+
new.raw_user_meta_data->>'avatar_url'
|
|
321
|
+
);
|
|
322
|
+
RETURN new;
|
|
323
|
+
END;
|
|
324
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
325
|
+
|
|
326
|
+
CREATE TRIGGER on_auth_user_created
|
|
327
|
+
AFTER INSERT ON auth.users
|
|
328
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
|
329
|
+
\`\`\`
|
|
330
|
+
|
|
331
|
+
## Password Management
|
|
332
|
+
\`\`\`typescript
|
|
333
|
+
// Update password (when logged in)
|
|
334
|
+
const { data, error } = await supabase.auth.updateUser({
|
|
335
|
+
password: 'new-password',
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Reset password (send email)
|
|
339
|
+
const { data, error } = await supabase.auth.resetPasswordForEmail(
|
|
340
|
+
'user@example.com',
|
|
341
|
+
{ redirectTo: \`\${window.location.origin}/auth/reset-password\` }
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// Handle reset callback
|
|
345
|
+
// app/auth/reset-password/page.tsx
|
|
346
|
+
export default function ResetPasswordPage() {
|
|
347
|
+
const handleReset = async (newPassword: string) => {
|
|
348
|
+
const { error } = await supabase.auth.updateUser({
|
|
349
|
+
password: newPassword,
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
\`\`\`
|
|
354
|
+
|
|
355
|
+
## ❌ DON'T
|
|
356
|
+
- Use anon key for server-side admin operations
|
|
357
|
+
- Skip RLS policies on user data tables
|
|
358
|
+
- Store session data manually (use SSR helpers)
|
|
359
|
+
- Forget to handle auth state changes
|
|
360
|
+
- Expose service role key to client
|
|
361
|
+
|
|
362
|
+
## ✅ DO
|
|
363
|
+
- Enable RLS on all user-related tables
|
|
364
|
+
- Use SSR package for Next.js App Router
|
|
365
|
+
- Handle auth callback for OAuth/magic links
|
|
366
|
+
- Create database triggers for user profiles
|
|
367
|
+
- Use middleware for session refresh
|
|
368
|
+
- Handle all auth state changes
|