naystack 1.2.25 → 1.3.1
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 +115 -1452
- package/dist/auth/email/client.d.mts +4 -2
- package/dist/auth/email/client.d.ts +4 -2
- package/dist/auth/email/index.cjs.js +2 -2
- package/dist/auth/email/index.esm.js +2 -2
- package/dist/auth/email/routes/post.cjs.js +1 -1
- package/dist/auth/email/routes/post.esm.js +1 -1
- package/dist/auth/email/routes/put.cjs.js +1 -1
- package/dist/auth/email/routes/put.esm.js +1 -1
- package/dist/auth/email/types.d.mts +2 -2
- package/dist/auth/email/types.d.ts +2 -2
- package/dist/auth/index.cjs.js +2 -2
- package/dist/auth/index.esm.js +2 -2
- package/dist/auth/types.d.mts +0 -2
- package/dist/auth/types.d.ts +0 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Naystack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A minimal, powerful stack for Next.js app development. Built with **Next.js + Drizzle ORM + GraphQL + S3 Auth**.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/naystack)
|
|
6
6
|
[](https://opensource.org/licenses/ISC)
|
|
@@ -8,1562 +8,225 @@
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npm install naystack
|
|
12
|
-
# or
|
|
13
11
|
pnpm add naystack
|
|
14
|
-
# or
|
|
15
|
-
yarn add naystack
|
|
16
12
|
```
|
|
17
13
|
|
|
18
|
-
## Modules
|
|
19
|
-
|
|
20
|
-
Naystack provides the following modules, each accessible via its own import path:
|
|
21
|
-
|
|
22
|
-
| Module | Import Path | Description |
|
|
23
|
-
| ----------- | -------------------------- | ------------------------------------------------------------------ |
|
|
24
|
-
| **Auth** | `naystack/auth` | Email, Google, and Instagram authentication (server-side routes) |
|
|
25
|
-
| **Auth UI** | `naystack/auth/email/client` | Client-side React hooks for login / signup / logout flows |
|
|
26
|
-
| **GraphQL** | `naystack/graphql` | GraphQL server initialization with type-graphql |
|
|
27
|
-
| **GQL App** | `naystack/graphql/client` / `naystack/graphql/server` | Apollo Client helpers for Next.js App Router (client + server) |
|
|
28
|
-
| **Client** | `naystack/client` | Generic client-side hooks and utilities |
|
|
29
|
-
| **File** | `naystack/file` | File upload to AWS S3 |
|
|
30
|
-
| **Socials** | `naystack/socials` | Instagram and Threads API integration |
|
|
31
|
-
|
|
32
14
|
---
|
|
33
15
|
|
|
34
|
-
##
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
import {
|
|
38
|
-
getEmailAuthRoutes,
|
|
39
|
-
initGoogleAuth,
|
|
40
|
-
initInstagramAuth,
|
|
41
|
-
} from "naystack/auth";
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### High-level Auth Flow in a Next.js App Router project
|
|
45
|
-
|
|
46
|
-
From a real-world setup (like `veas-web`), the pieces look like this:
|
|
47
|
-
|
|
48
|
-
- **Server-side auth routes** – created with `getEmailAuthRoutes` (and optionally Google / Instagram)
|
|
49
|
-
- **Server-side GraphQL context** – uses `getUserIdFromRequest` from the email auth routes
|
|
50
|
-
- **Client-side auth hooks** – created with `getEmailAuthUtils` and used in login/signup/logout forms
|
|
51
|
-
|
|
52
|
-
The sections below show each part in more detail.
|
|
53
|
-
|
|
54
|
-
### Email Authentication
|
|
55
|
-
|
|
56
|
-
Setup email-based authentication with JWT tokens and optional Turnstile captcha verification.
|
|
57
|
-
|
|
58
|
-
**Next.js App Router Example (server routes):**
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
const emailAuth = getEmailAuthRoutes({
|
|
62
|
-
getUser: async (email: string) => { /* fetch user by email */ },
|
|
63
|
-
createUser: async (user: UserInput) => { /* create new user */ },
|
|
64
|
-
signingKey: process.env.JWT_SIGNING_KEY!,
|
|
65
|
-
refreshKey: process.env.JWT_REFRESH_KEY!,
|
|
66
|
-
turnstileKey?: string, // Optional: Cloudflare Turnstile secret key
|
|
67
|
-
onSignUp: (user) => { /* callback on signup */ },
|
|
68
|
-
onLogout?: (body) => { /* callback on logout */ },
|
|
69
|
-
onError?: (error) => { /* custom error handler */ },
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Export in Next.js route handler
|
|
73
|
-
export const { GET, POST, PUT, DELETE, getUserIdFromRequest } = emailAuth;
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
**Client-side hooks (login, signup, logout) – as used in `veas-web`:**
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
// app/(auth)/utils.ts
|
|
80
|
-
import { getEmailAuthUtils } from "naystack/auth/email/client";
|
|
81
|
-
|
|
82
|
-
// The argument is the URL where your email auth API lives
|
|
83
|
-
export const { useLogin, useLogout, useSignUp } =
|
|
84
|
-
getEmailAuthUtils("/api/email");
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
// app/(auth)/login/components/form.tsx
|
|
89
|
-
"use client";
|
|
90
|
-
import React, { useState } from "react";
|
|
91
|
-
import { useLogin } from "@/app/(auth)/utils";
|
|
92
|
-
import { useRouter } from "next/navigation";
|
|
93
|
-
|
|
94
|
-
export default function LoginForm() {
|
|
95
|
-
const login = useLogin();
|
|
96
|
-
const router = useRouter();
|
|
97
|
-
const [email, setEmail] = useState("");
|
|
98
|
-
const [password, setPassword] = useState("");
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<form
|
|
102
|
-
onSubmit={async (e) => {
|
|
103
|
-
e.preventDefault();
|
|
104
|
-
const ok = await login({ email, password });
|
|
105
|
-
if (ok) router.push("/dashboard");
|
|
106
|
-
}}
|
|
107
|
-
>
|
|
108
|
-
{/* inputs + submit */}
|
|
109
|
-
</form>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
// app/(auth)/signup/components/form.tsx
|
|
116
|
-
"use client";
|
|
117
|
-
import React, { useState } from "react";
|
|
118
|
-
import { useSignUp } from "@/app/(auth)/utils";
|
|
119
|
-
import { useRouter } from "next/navigation";
|
|
120
|
-
|
|
121
|
-
export default function SignupForm() {
|
|
122
|
-
const signUp = useSignUp();
|
|
123
|
-
const router = useRouter();
|
|
124
|
-
const [email, setEmail] = useState("");
|
|
125
|
-
const [password, setPassword] = useState("");
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<form
|
|
129
|
-
onSubmit={async (e) => {
|
|
130
|
-
e.preventDefault();
|
|
131
|
-
const ok = await signUp({ email, password });
|
|
132
|
-
if (ok) router.push("/onboard");
|
|
133
|
-
}}
|
|
134
|
-
>
|
|
135
|
-
{/* inputs + submit */}
|
|
136
|
-
</form>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
// app/dashboard/components/logout-button.tsx
|
|
143
|
-
"use client";
|
|
144
|
-
import React, { useState } from "react";
|
|
145
|
-
import { useLogout } from "@/app/(auth)/utils";
|
|
146
|
-
import { useRouter } from "next/navigation";
|
|
16
|
+
## 1. Authentication
|
|
147
17
|
|
|
148
|
-
|
|
149
|
-
const logout = useLogout();
|
|
150
|
-
const [loading, setLoading] = useState(false);
|
|
151
|
-
const router = useRouter();
|
|
152
|
-
|
|
153
|
-
const handleLogout = async () => {
|
|
154
|
-
setLoading(true);
|
|
155
|
-
try {
|
|
156
|
-
await logout();
|
|
157
|
-
router.push("/login");
|
|
158
|
-
} finally {
|
|
159
|
-
setLoading(false);
|
|
160
|
-
}
|
|
161
|
-
};
|
|
18
|
+
Naystack provides a seamless email-based authentication system with optional support for Google and Instagram.
|
|
162
19
|
|
|
163
|
-
|
|
164
|
-
<button onClick={handleLogout} disabled={loading}>
|
|
165
|
-
{loading ? "Logging out..." : "Logout"}
|
|
166
|
-
</button>
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
```
|
|
20
|
+
### Server Setup
|
|
170
21
|
|
|
171
|
-
|
|
22
|
+
Define your auth routes in `app/api/(auth)/email/index.ts`:
|
|
172
23
|
|
|
173
24
|
```typescript
|
|
174
|
-
import { db } from "@/lib/db";
|
|
175
|
-
import { UserTable, WebPushSubscriptionTable } from "@/lib/db/schema";
|
|
176
|
-
import { eq } from "drizzle-orm";
|
|
177
25
|
import { getEmailAuthRoutes } from "naystack/auth";
|
|
178
|
-
import {
|
|
179
|
-
|
|
180
|
-
export const { GET, POST, PUT, DELETE, getUserIdFromRequest } =
|
|
181
|
-
getEmailAuthRoutes({
|
|
182
|
-
// Fetch user by email using Drizzle
|
|
183
|
-
getUser: (email: string) =>
|
|
184
|
-
db.query.UserTable.findFirst({
|
|
185
|
-
where: eq(UserTable.email, email)
|
|
186
|
-
}),
|
|
187
|
-
|
|
188
|
-
// Create new user
|
|
189
|
-
createUser: async (user) => {
|
|
190
|
-
const [newUser] = await db
|
|
191
|
-
.insert(UserTable)
|
|
192
|
-
.values(user)
|
|
193
|
-
.returning();
|
|
194
|
-
return newUser;
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
signingKey: process.env.SIGNING_KEY!,
|
|
198
|
-
refreshKey: process.env.REFRESH_KEY!,
|
|
199
|
-
turnstileKey: process.env.TURNSTILE_KEY!,
|
|
200
|
-
|
|
201
|
-
// Send welcome email on signup
|
|
202
|
-
onSignUp: ({ id, email }) =>
|
|
203
|
-
waitUntil(
|
|
204
|
-
(async () => {
|
|
205
|
-
const link = await getVerificationLink(id);
|
|
206
|
-
if (link && email) {
|
|
207
|
-
await sendTemplateEmail(email, "WelcomeUser", {
|
|
208
|
-
verifyLink: link,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
})(),
|
|
212
|
-
),
|
|
213
|
-
|
|
214
|
-
// Clean up push subscriptions on logout
|
|
215
|
-
onLogout: async (endpoint: string) => {
|
|
216
|
-
await db
|
|
217
|
-
.delete(WebPushSubscriptionTable)
|
|
218
|
-
.where(eq(WebPushSubscriptionTable.endpoint, endpoint));
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
#### Options
|
|
224
|
-
|
|
225
|
-
| Option | Type | Required | Description |
|
|
226
|
-
| -------------- | ------------------------------------------------------- | -------- | ------------------------------- |
|
|
227
|
-
| `getUser` | `(email: string) => Promise<UserOutput \| undefined>` | ✅ | Fetch user by email |
|
|
228
|
-
| `createUser` | `(user: UserInput) => Promise<UserOutput \| undefined>` | ✅ | Create new user |
|
|
229
|
-
| `signingKey` | `string` | ✅ | JWT signing key |
|
|
230
|
-
| `refreshKey` | `string` | ✅ | JWT refresh key |
|
|
231
|
-
| `turnstileKey` | `string` | ❌ | Cloudflare Turnstile secret key |
|
|
232
|
-
| `onSignUp` | `(user: UserOutput) => void` | ✅ | Callback when user signs up |
|
|
233
|
-
| `onLogout` | `(body: string) => Promise<void>` | ❌ | Callback on logout |
|
|
234
|
-
| `onError` | `ErrorHandler` | ❌ | Custom error handler |
|
|
235
|
-
|
|
236
|
-
#### Types
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
239
|
-
interface UserInput {
|
|
240
|
-
email: string;
|
|
241
|
-
password: string;
|
|
242
|
-
[key: string]: unknown;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
interface UserOutput {
|
|
246
|
-
id: number;
|
|
247
|
-
email: string;
|
|
248
|
-
password: string | null;
|
|
249
|
-
[key: string]: unknown;
|
|
250
|
-
}
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
---
|
|
254
|
-
|
|
255
|
-
### Google Authentication
|
|
256
|
-
|
|
257
|
-
**Real-World Example:**
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
import { db } from "@/lib/db";
|
|
261
|
-
import { UserTable } from "@/lib/db/schema";
|
|
26
|
+
import { db } from "@/app/api/lib/db";
|
|
27
|
+
import { UserTable } from "@/app/api/(graphql)/User/db";
|
|
262
28
|
import { eq } from "drizzle-orm";
|
|
263
|
-
import { initGoogleAuth } from "naystack/auth";
|
|
264
|
-
|
|
265
|
-
export const { GET } = initGoogleAuth({
|
|
266
|
-
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
267
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
268
|
-
authRoute: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/google`,
|
|
269
|
-
successRedirectURL: "/dashboard",
|
|
270
|
-
errorRedirectURL: "/login?error=google",
|
|
271
|
-
refreshKey: process.env.REFRESH_KEY!,
|
|
272
|
-
|
|
273
|
-
// Find existing user or create new one
|
|
274
|
-
getUserIdFromEmail: async ({ email, name }) => {
|
|
275
|
-
if (!email) return null;
|
|
276
|
-
|
|
277
|
-
// Update existing user's email verification status
|
|
278
|
-
const [existingUser] = await db
|
|
279
|
-
.update(UserTable)
|
|
280
|
-
.set({ emailVerified: true })
|
|
281
|
-
.where(eq(UserTable.email, email))
|
|
282
|
-
.returning({ id: UserTable.id });
|
|
283
|
-
|
|
284
|
-
if (existingUser) {
|
|
285
|
-
return existingUser.id;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Create new user if doesn't exist
|
|
289
|
-
if (name && email) {
|
|
290
|
-
const [newUser] = await db
|
|
291
|
-
.insert(UserTable)
|
|
292
|
-
.values({
|
|
293
|
-
email,
|
|
294
|
-
name,
|
|
295
|
-
emailVerified: true,
|
|
296
|
-
})
|
|
297
|
-
.returning({ id: UserTable.id });
|
|
298
|
-
|
|
299
|
-
return newUser?.id || null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return null;
|
|
303
|
-
},
|
|
304
|
-
});
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
#### Options
|
|
308
|
-
|
|
309
|
-
| Option | Type | Required | Description |
|
|
310
|
-
| -------------------- | ----------------------------------------------------- | -------- | --------------------------------- |
|
|
311
|
-
| `clientId` | `string` | ✅ | Google OAuth client ID |
|
|
312
|
-
| `clientSecret` | `string` | ✅ | Google OAuth client secret |
|
|
313
|
-
| `authRoute` | `string` | ✅ | OAuth callback route |
|
|
314
|
-
| `successRedirectURL` | `string` | ✅ | Redirect URL on success |
|
|
315
|
-
| `errorRedirectURL` | `string` | ✅ | Redirect URL on error |
|
|
316
|
-
| `refreshKey` | `string` | ✅ | JWT refresh key |
|
|
317
|
-
| `getUserIdFromEmail` | `(email: Schema$Userinfo) => Promise<number \| null>` | ✅ | Get user ID from Google user info |
|
|
318
29
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
export const { GET, getRefreshedAccessToken } = initInstagramAuth({
|
|
332
|
-
clientId: process.env.NEXT_PUBLIC_INSTAGRAM_CLIENT_ID!,
|
|
333
|
-
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET!,
|
|
334
|
-
authRoute: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/instagram`,
|
|
335
|
-
successRedirectURL: "/profile",
|
|
336
|
-
errorRedirectURL: "/signup",
|
|
337
|
-
refreshKey: process.env.REFRESH_KEY!,
|
|
338
|
-
|
|
339
|
-
// Verify and link Instagram account
|
|
340
|
-
onUser: async (instagramData, userId, accessToken) => {
|
|
341
|
-
if (!userId) return "You are not logged in";
|
|
342
|
-
|
|
343
|
-
const user = await db.query.UserTable.findFirst({
|
|
344
|
-
where: eq(UserTable.id, userId),
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
if (!user?.instagramDetails) {
|
|
348
|
-
return "Please connect Instagram first";
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Update Instagram verification status
|
|
352
|
-
const [updated] = await db
|
|
353
|
-
.update(InstagramDetails)
|
|
354
|
-
.set({
|
|
355
|
-
isVerified: true,
|
|
356
|
-
accessToken: accessToken
|
|
30
|
+
export const { GET, POST, PUT, DELETE, getContext } = getEmailAuthRoutes({
|
|
31
|
+
createUser: async (data) => {
|
|
32
|
+
const [user] = await db.insert(UserTable).values(data).returning();
|
|
33
|
+
return user;
|
|
34
|
+
},
|
|
35
|
+
getUser: async (email) => {
|
|
36
|
+
const [user] = await db
|
|
37
|
+
.select({
|
|
38
|
+
id: UserTable.id,
|
|
39
|
+
email: UserTable.email,
|
|
40
|
+
password: UserTable.password,
|
|
357
41
|
})
|
|
358
|
-
.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
eq(InstagramDetails.username, instagramData.username),
|
|
362
|
-
),
|
|
363
|
-
)
|
|
364
|
-
.returning({ username: InstagramDetails.username });
|
|
365
|
-
|
|
366
|
-
if (!updated) {
|
|
367
|
-
return "Please login with the same username as the connected account";
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Disconnect from other users if linked
|
|
371
|
-
await db
|
|
372
|
-
.update(UserTable)
|
|
373
|
-
.set({ instagramDetails: null })
|
|
374
|
-
.where(
|
|
375
|
-
and(
|
|
376
|
-
ne(UserTable.id, userId),
|
|
377
|
-
eq(UserTable.instagramDetails, user.instagramDetails),
|
|
378
|
-
),
|
|
379
|
-
);
|
|
42
|
+
.from(UserTable)
|
|
43
|
+
.where(eq(UserTable.email, email));
|
|
44
|
+
return user;
|
|
380
45
|
},
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
#### Options
|
|
385
|
-
|
|
386
|
-
| Option | Type | Required | Description |
|
|
387
|
-
| -------------------- | ------------------------------------------------------------------------------------------- | -------- | --------------------------------- |
|
|
388
|
-
| `clientId` | `string` | ✅ | Instagram app client ID |
|
|
389
|
-
| `clientSecret` | `string` | ✅ | Instagram app client secret |
|
|
390
|
-
| `authRoute` | `string` | ✅ | OAuth callback route |
|
|
391
|
-
| `successRedirectURL` | `string` | ✅ | Redirect URL on success |
|
|
392
|
-
| `errorRedirectURL` | `string` | ✅ | Redirect URL on error |
|
|
393
|
-
| `refreshKey` | `string` | ✅ | JWT refresh key |
|
|
394
|
-
| `onUser` | `(data: InstagramUser, id: number \| null, accessToken: string) => Promise<string \| void>` | ✅ | Callback with Instagram user data |
|
|
395
|
-
|
|
396
|
-
---
|
|
397
|
-
|
|
398
|
-
## GraphQL Module
|
|
399
|
-
|
|
400
|
-
```typescript
|
|
401
|
-
import {
|
|
402
|
-
initGraphQLServer,
|
|
403
|
-
GQLError,
|
|
404
|
-
query,
|
|
405
|
-
field,
|
|
406
|
-
QueryLibrary,
|
|
407
|
-
FieldLibrary,
|
|
408
|
-
} from "naystack/graphql";
|
|
409
|
-
import type { Context, AuthorizedContext } from "naystack/graphql";
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### Initialize GraphQL Server (Next.js App Router)
|
|
413
|
-
|
|
414
|
-
**Basic Example:**
|
|
415
|
-
|
|
416
|
-
```typescript
|
|
417
|
-
const { GET, POST } = await initGraphQLServer({
|
|
418
|
-
resolvers: [UserResolver, PostResolver],
|
|
419
|
-
authChecker: ({ context }) => !!context.userId,
|
|
420
|
-
plugins: [], // Optional Apollo plugins
|
|
421
|
-
context: async (req) => ({
|
|
422
|
-
// Custom context builder
|
|
423
|
-
userId: await getUserIdFromRequest(req),
|
|
424
|
-
}),
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
export { GET, POST };
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
**Real-World Example with Advanced Context:**
|
|
431
|
-
|
|
432
|
-
```typescript
|
|
433
|
-
import { initGraphQLServer } from "naystack/graphql";
|
|
434
|
-
import { getUserIdFromRequest } from "@/api/(auth)/email/setup";
|
|
435
|
-
import { authChecker } from "@/lib/auth/context";
|
|
436
|
-
|
|
437
|
-
export const { GET, POST } = await initGraphQLServer({
|
|
438
|
-
resolvers: [
|
|
439
|
-
UserResolver,
|
|
440
|
-
PostResolver,
|
|
441
|
-
ApplicationResolver,
|
|
442
|
-
ChatResolver,
|
|
443
|
-
// ... more resolvers
|
|
444
|
-
],
|
|
445
|
-
authChecker,
|
|
446
|
-
context: async (req) => {
|
|
447
|
-
const res = getUserIdFromRequest(req);
|
|
448
|
-
if (!res) return { userId: null };
|
|
449
|
-
|
|
450
|
-
// Handle refresh token user ID
|
|
451
|
-
if (res.refreshUserID) {
|
|
452
|
-
const isMobile = req.headers.get("x-platform-is-mobile");
|
|
453
|
-
if (isMobile) return { userId: null };
|
|
454
|
-
return { userId: res.refreshUserID, onlyQuery: true };
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Handle access token user ID
|
|
458
|
-
if (res.accessUserId) {
|
|
459
|
-
return { userId: res.accessUserId };
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return { userId: null };
|
|
46
|
+
keys: {
|
|
47
|
+
signing: process.env.SIGNING_KEY!,
|
|
48
|
+
refresh: process.env.REFRESH_KEY!,
|
|
463
49
|
},
|
|
464
50
|
});
|
|
465
51
|
```
|
|
466
52
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
In a typical app you will:
|
|
470
|
-
|
|
471
|
-
- Define **GraphQL resolvers** using `QueryLibrary` / `FieldLibrary`
|
|
472
|
-
- Initialize the GraphQL **API route** with `initGraphQLServer`
|
|
473
|
-
- Use **server-side helper** `getGraphQLQuery` to call GraphQL from RSC / server actions
|
|
474
|
-
- Use **client-side helper** `getApolloWrapper` and `useAuthMutation` for client components
|
|
475
|
-
|
|
476
|
-
The next sections show how these pieces fit together.
|
|
477
|
-
|
|
478
|
-
#### Options
|
|
479
|
-
|
|
480
|
-
| Option | Type | Required | Description |
|
|
481
|
-
| ------------- | ------------------------------------ | -------- | -------------------------------- |
|
|
482
|
-
| `resolvers` | `NonEmptyArray<Function>` | ✅ | Array of TypeGraphQL resolvers |
|
|
483
|
-
| `authChecker` | `AuthChecker<any>` | ❌ | Custom auth checker function |
|
|
484
|
-
| `plugins` | `ApolloServerPlugin[]` | ❌ | Additional Apollo Server plugins |
|
|
485
|
-
| `context` | `(req: NextRequest) => Promise<any>` | ❌ | Context builder function |
|
|
486
|
-
|
|
487
|
-
### Error Handling
|
|
53
|
+
> **Note:** Google and Instagram auth are also available via `initGoogleAuth` and `initInstagramAuth` from `naystack/auth`.
|
|
488
54
|
|
|
489
|
-
|
|
55
|
+
### Client Setup
|
|
490
56
|
|
|
491
|
-
|
|
492
|
-
import { GQLError } from "naystack/graphql";
|
|
493
|
-
|
|
494
|
-
// Usage in resolvers
|
|
495
|
-
throw GQLError(404, "User not found");
|
|
496
|
-
throw GQLError(403); // "You are not allowed to perform this action"
|
|
497
|
-
throw GQLError(400); // "Please provide all required inputs"
|
|
498
|
-
throw GQLError(); // "Server Error" (500)
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
**Real-World Example in Resolvers:**
|
|
57
|
+
Wrap your application with `AuthWrapper` in your root layout or a client component.
|
|
502
58
|
|
|
503
59
|
```typescript
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
import {
|
|
507
|
-
import { eq } from "drizzle-orm";
|
|
508
|
-
|
|
509
|
-
export async function createPosting(
|
|
510
|
-
ctx: Context,
|
|
511
|
-
input: NewPostingInput,
|
|
512
|
-
): Promise<number | null> {
|
|
513
|
-
// Authentication check
|
|
514
|
-
if (!ctx.userId) {
|
|
515
|
-
throw GQLError(400, "Please login to create posting");
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const user = await db.query.UserTable.findFirst({
|
|
519
|
-
where: eq(UserTable.id, ctx.userId),
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
// Authorization check
|
|
523
|
-
if (!user || user.role === "CREATOR") {
|
|
524
|
-
throw GQLError(400, "Only onboarded users can create postings");
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Validation check
|
|
528
|
-
if (!user.emailVerified) {
|
|
529
|
-
throw GQLError(400, "Please verify email to create posting");
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Rate limiting check
|
|
533
|
-
const yesterday = new Date();
|
|
534
|
-
yesterday.setDate(yesterday.getDate() - 1);
|
|
535
|
-
const recentPostings = await db.query.PostingTable.findMany({
|
|
536
|
-
where: and(
|
|
537
|
-
eq(PostingTable.userId, ctx.userId),
|
|
538
|
-
gte(PostingTable.createdAt, yesterday),
|
|
539
|
-
),
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
if (recentPostings.length >= MAXIMUM_POSTINGS_DAY) {
|
|
543
|
-
throw GQLError(
|
|
544
|
-
400,
|
|
545
|
-
`Only ${MAXIMUM_POSTINGS_DAY} allowed in 24 hours. Try again later.`,
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Create posting...
|
|
550
|
-
}
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
### Query & Field Helpers
|
|
554
|
-
|
|
555
|
-
Build resolvers functionally using `query`, `field`, `QueryLibrary`, and `FieldLibrary`:
|
|
556
|
-
|
|
557
|
-
**Basic Example:**
|
|
60
|
+
// gql/client.ts
|
|
61
|
+
"use client";
|
|
62
|
+
import { getAuthWrapper } from "naystack/auth/email/client";
|
|
558
63
|
|
|
559
|
-
|
|
560
|
-
import { query, QueryLibrary, field, FieldLibrary } from "naystack/graphql";
|
|
561
|
-
|
|
562
|
-
// Define queries/mutations
|
|
563
|
-
const queries = {
|
|
564
|
-
getUser: query(
|
|
565
|
-
async (ctx, input) => {
|
|
566
|
-
return await db.query.users.findFirst({ where: eq(users.id, input) });
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
output: User,
|
|
570
|
-
input: Number,
|
|
571
|
-
authorized: true,
|
|
572
|
-
}
|
|
573
|
-
),
|
|
574
|
-
createUser: query(
|
|
575
|
-
async (ctx, input) => {
|
|
576
|
-
/* ... */
|
|
577
|
-
},
|
|
578
|
-
{
|
|
579
|
-
output: User,
|
|
580
|
-
input: CreateUserInput,
|
|
581
|
-
mutation: true, // Makes this a mutation instead of query
|
|
582
|
-
}
|
|
583
|
-
),
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
// Generate resolver class
|
|
587
|
-
const UserResolver = QueryLibrary(queries);
|
|
588
|
-
|
|
589
|
-
// Define field resolvers
|
|
590
|
-
const fields = {
|
|
591
|
-
posts: field(
|
|
592
|
-
async (root, ctx) => {
|
|
593
|
-
return await db.query.posts.findMany({
|
|
594
|
-
where: eq(posts.userId, root.id),
|
|
595
|
-
});
|
|
596
|
-
},
|
|
597
|
-
{ output: [Post] }
|
|
598
|
-
),
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
const UserFieldResolver = FieldLibrary(User, fields);
|
|
64
|
+
export const AuthWrapper = getAuthWrapper("/api/email");
|
|
602
65
|
```
|
|
603
66
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
```typescript
|
|
607
|
-
// app/api/(graphql)/User/db.ts
|
|
608
|
-
import { pgTable, serial, text, timestamp, real } from "drizzle-orm/pg-core";
|
|
609
|
-
|
|
610
|
-
export const UserTable = pgTable("users", {
|
|
611
|
-
id: serial("id").primaryKey(),
|
|
612
|
-
email: text("email").notNull(),
|
|
613
|
-
password: text("password").notNull(),
|
|
614
|
-
name: text("name"),
|
|
615
|
-
dateOfBirth: timestamp("date_of_birth"),
|
|
616
|
-
placeOfBirthLat: real("place_of_birth_lat"),
|
|
617
|
-
placeOfBirthLong: real("place_of_birth_long"),
|
|
618
|
-
timezone: real("timezone"),
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
export type UserDB = typeof UserTable.$inferSelect;
|
|
622
|
-
```
|
|
67
|
+
### Frontend Usage
|
|
623
68
|
|
|
624
69
|
```typescript
|
|
625
|
-
|
|
626
|
-
import { Field, ObjectType } from "type-graphql";
|
|
627
|
-
|
|
628
|
-
@ObjectType("User")
|
|
629
|
-
export class User {
|
|
630
|
-
@Field()
|
|
631
|
-
id: number;
|
|
70
|
+
import { getEmailAuthUtils, useToken } from "naystack/auth/email/client";
|
|
632
71
|
|
|
633
|
-
|
|
634
|
-
email: string;
|
|
72
|
+
const { useLogin, useSignUp, useLogout } = getEmailAuthUtils("/api/email");
|
|
635
73
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
@Field(() => Number, { nullable: true })
|
|
643
|
-
placeOfBirthLat: number | null;
|
|
644
|
-
|
|
645
|
-
@Field(() => Number, { nullable: true })
|
|
646
|
-
placeOfBirthLong: number | null;
|
|
647
|
-
|
|
648
|
-
@Field(() => Number, { nullable: true })
|
|
649
|
-
timezone: number | null;
|
|
74
|
+
function AuthComponent() {
|
|
75
|
+
const login = useLogin();
|
|
76
|
+
const signup = useSignUp();
|
|
77
|
+
const logout = useLogout();
|
|
78
|
+
const token = useToken(); // Get current JWT token
|
|
650
79
|
|
|
651
|
-
|
|
652
|
-
isOnboarded: boolean;
|
|
80
|
+
// ... forms and handlers
|
|
653
81
|
}
|
|
654
82
|
```
|
|
655
83
|
|
|
656
|
-
|
|
657
|
-
// app/api/(graphql)/User/resolvers/is-onboarded.ts
|
|
658
|
-
import { field } from "naystack/graphql";
|
|
659
|
-
import type { UserDB } from "../db";
|
|
660
|
-
|
|
661
|
-
export default field(
|
|
662
|
-
async (user: UserDB) => {
|
|
663
|
-
return (
|
|
664
|
-
!!user.name &&
|
|
665
|
-
!!user.dateOfBirth &&
|
|
666
|
-
user.placeOfBirthLat !== null &&
|
|
667
|
-
user.placeOfBirthLong !== null &&
|
|
668
|
-
user.timezone !== null
|
|
669
|
-
);
|
|
670
|
-
},
|
|
671
|
-
{ output: Boolean },
|
|
672
|
-
);
|
|
673
|
-
```
|
|
84
|
+
---
|
|
674
85
|
|
|
675
|
-
|
|
676
|
-
// app/api/(graphql)/User/resolvers/get-current-user.ts
|
|
677
|
-
import { query } from "naystack/graphql";
|
|
678
|
-
import { db } from "@/app/api/lib/db";
|
|
679
|
-
import { UserTable } from "../db";
|
|
680
|
-
import { eq } from "drizzle-orm";
|
|
681
|
-
import { User } from "../types";
|
|
86
|
+
## 2. GraphQL
|
|
682
87
|
|
|
683
|
-
|
|
684
|
-
async (ctx: { userId: number | null }) => {
|
|
685
|
-
if (!ctx.userId) return null;
|
|
686
|
-
const [user] = await db
|
|
687
|
-
.select()
|
|
688
|
-
.from(UserTable)
|
|
689
|
-
.where(eq(UserTable.id, ctx.userId));
|
|
690
|
-
return user || null;
|
|
691
|
-
},
|
|
692
|
-
{
|
|
693
|
-
output: User,
|
|
694
|
-
},
|
|
695
|
-
);
|
|
696
|
-
```
|
|
88
|
+
Naystack leverages `type-graphql` and `apollo-server` for a type-safe GraphQL experience.
|
|
697
89
|
|
|
698
|
-
|
|
699
|
-
// app/api/(graphql)/User/graphql.ts
|
|
700
|
-
import { QueryLibrary, FieldLibrary } from "naystack/graphql";
|
|
701
|
-
import getCurrentUser from "./resolvers/get-current-user";
|
|
702
|
-
import onboardUser from "./resolvers/onboard-user";
|
|
703
|
-
import isOnboarded from "./resolvers/is-onboarded";
|
|
704
|
-
import type { UserDB } from "./db";
|
|
705
|
-
import { User } from "./types";
|
|
706
|
-
|
|
707
|
-
export const UserResolvers = QueryLibrary({
|
|
708
|
-
getCurrentUser,
|
|
709
|
-
onboardUser,
|
|
710
|
-
});
|
|
90
|
+
### Server Setup
|
|
711
91
|
|
|
712
|
-
|
|
713
|
-
isOnboarded,
|
|
714
|
-
});
|
|
715
|
-
```
|
|
92
|
+
Initialize your GraphQL server in `app/api/(graphql)/route.ts`:
|
|
716
93
|
|
|
717
94
|
```typescript
|
|
718
|
-
// app/api/(graphql)/route.ts
|
|
719
95
|
import { initGraphQLServer } from "naystack/graphql";
|
|
720
|
-
import {
|
|
721
|
-
import {
|
|
96
|
+
import { getContext } from "@/app/api/(auth)/email";
|
|
97
|
+
import { UserResolvers } from "./User/graphql";
|
|
722
98
|
|
|
723
99
|
export const { GET, POST } = await initGraphQLServer({
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const res = getUserIdFromRequest(req);
|
|
727
|
-
if (!res) return { userId: null };
|
|
728
|
-
return {
|
|
729
|
-
userId: res.accessUserId ?? res.refreshUserID ?? null,
|
|
730
|
-
};
|
|
731
|
-
},
|
|
100
|
+
getContext,
|
|
101
|
+
resolvers: [UserResolvers],
|
|
732
102
|
});
|
|
733
103
|
```
|
|
734
104
|
|
|
735
|
-
|
|
105
|
+
### Server Component Query
|
|
736
106
|
|
|
737
|
-
|
|
107
|
+
Call your GraphQL API from server components or actions:
|
|
738
108
|
|
|
739
109
|
```typescript
|
|
740
|
-
|
|
741
|
-
import {
|
|
742
|
-
import { NotificationTable } from "@/lib/db/schema";
|
|
743
|
-
import { eq, lte } from "drizzle-orm";
|
|
744
|
-
import { waitUntil } from "@vercel/functions";
|
|
745
|
-
|
|
746
|
-
// Query with side effects
|
|
747
|
-
export const getNotifications = query(
|
|
748
|
-
async (ctx) => {
|
|
749
|
-
if (!ctx.userId) return [];
|
|
750
|
-
|
|
751
|
-
// Mark all as read
|
|
752
|
-
const notifications = await db
|
|
753
|
-
.update(NotificationTable)
|
|
754
|
-
.set({ read: true })
|
|
755
|
-
.where(eq(NotificationTable.user, ctx.userId))
|
|
756
|
-
.returning();
|
|
757
|
-
|
|
758
|
-
// Clean up old notifications (async)
|
|
759
|
-
const weekBefore = new Date();
|
|
760
|
-
weekBefore.setDate(weekBefore.getDate() - 7);
|
|
761
|
-
waitUntil(
|
|
762
|
-
db
|
|
763
|
-
.delete(NotificationTable)
|
|
764
|
-
.where(lte(NotificationTable.createdAt, weekBefore)),
|
|
765
|
-
);
|
|
766
|
-
|
|
767
|
-
return notifications.sort((a, b) => a.id - b.id);
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
output: [NotificationGQL!],
|
|
771
|
-
},
|
|
772
|
-
);
|
|
110
|
+
// gql/server.ts
|
|
111
|
+
import { getGraphQLQuery } from "naystack/graphql/server";
|
|
773
112
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
getNotifications,
|
|
777
|
-
getUnreadNotifications,
|
|
113
|
+
export const query = getGraphQLQuery({
|
|
114
|
+
uri: process.env.NEXT_PUBLIC_BACKEND_BASE_URL!,
|
|
778
115
|
});
|
|
779
116
|
|
|
780
|
-
//
|
|
781
|
-
|
|
782
|
-
isOnboarded: field(
|
|
783
|
-
async (user) => {
|
|
784
|
-
return getIsOnboarded(user);
|
|
785
|
-
},
|
|
786
|
-
{
|
|
787
|
-
output: Boolean,
|
|
788
|
-
}
|
|
789
|
-
),
|
|
790
|
-
});
|
|
117
|
+
// Usage in Page
|
|
118
|
+
const data = await query(MyQueryDocument, { variables });
|
|
791
119
|
```
|
|
792
120
|
|
|
793
|
-
###
|
|
121
|
+
### Client Setup (Apollo)
|
|
794
122
|
|
|
795
|
-
|
|
796
|
-
interface Context {
|
|
797
|
-
userId: number | null;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
interface AuthorizedContext {
|
|
801
|
-
userId: number;
|
|
802
|
-
}
|
|
803
|
-
```
|
|
804
|
-
|
|
805
|
-
---
|
|
806
|
-
|
|
807
|
-
## Client Module
|
|
808
|
-
|
|
809
|
-
```typescript
|
|
810
|
-
import {
|
|
811
|
-
useVisibility,
|
|
812
|
-
useBreakpoint,
|
|
813
|
-
setupSEO,
|
|
814
|
-
getHandleImageUpload,
|
|
815
|
-
getInstagramAuthorizationURLSetup,
|
|
816
|
-
} from "naystack/client";
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
### Hooks
|
|
820
|
-
|
|
821
|
-
#### `useVisibility`
|
|
822
|
-
|
|
823
|
-
Observe element visibility using Intersection Observer. Perfect for infinite scroll and lazy loading.
|
|
824
|
-
|
|
825
|
-
**Basic Example:**
|
|
826
|
-
|
|
827
|
-
```typescript
|
|
828
|
-
function Component() {
|
|
829
|
-
const ref = useVisibility(() => {
|
|
830
|
-
console.log("Element is visible!");
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
return <div ref={ref}>Watch me!</div>;
|
|
834
|
-
}
|
|
835
|
-
```
|
|
836
|
-
|
|
837
|
-
**Real-World Example - Infinite Scroll:**
|
|
838
|
-
|
|
839
|
-
```typescript
|
|
840
|
-
"use client";
|
|
841
|
-
|
|
842
|
-
import { useVisibility } from "naystack/client";
|
|
843
|
-
import { useRef } from "react";
|
|
844
|
-
|
|
845
|
-
export default function PostingCard({
|
|
846
|
-
posting,
|
|
847
|
-
fetchMore
|
|
848
|
-
}: {
|
|
849
|
-
posting: Posting;
|
|
850
|
-
fetchMore?: () => void;
|
|
851
|
-
}) {
|
|
852
|
-
// Trigger fetchMore when card becomes visible
|
|
853
|
-
const mainRef = useVisibility(fetchMore);
|
|
854
|
-
|
|
855
|
-
return (
|
|
856
|
-
<div ref={mainRef} className="posting-card">
|
|
857
|
-
<h3>{posting.title}</h3>
|
|
858
|
-
<p>{posting.description}</p>
|
|
859
|
-
</div>
|
|
860
|
-
);
|
|
861
|
-
}
|
|
862
|
-
```
|
|
863
|
-
|
|
864
|
-
#### `useBreakpoint`
|
|
865
|
-
|
|
866
|
-
React to media query changes. Useful for responsive layouts and conditional rendering.
|
|
867
|
-
|
|
868
|
-
**Basic Example:**
|
|
869
|
-
|
|
870
|
-
```typescript
|
|
871
|
-
function Component() {
|
|
872
|
-
const isMobile = useBreakpoint("(max-width: 768px)");
|
|
873
|
-
|
|
874
|
-
return <div>{isMobile ? "Mobile" : "Desktop"}</div>;
|
|
875
|
-
}
|
|
876
|
-
```
|
|
877
|
-
|
|
878
|
-
**Real-World Example - Responsive Layout:**
|
|
879
|
-
|
|
880
|
-
```typescript
|
|
881
|
-
"use client";
|
|
882
|
-
|
|
883
|
-
import { useBreakpoint } from "naystack/client";
|
|
884
|
-
|
|
885
|
-
export default function LayoutWrapper({
|
|
886
|
-
children
|
|
887
|
-
}: {
|
|
888
|
-
children: React.ReactNode;
|
|
889
|
-
}) {
|
|
890
|
-
const isLarge = useBreakpoint("(min-width: 1024px)");
|
|
891
|
-
|
|
892
|
-
return (
|
|
893
|
-
<div
|
|
894
|
-
style={{
|
|
895
|
-
height: isLarge
|
|
896
|
-
? "calc(100svh - 80px)"
|
|
897
|
-
: "calc(100svh - 55px)",
|
|
898
|
-
}}
|
|
899
|
-
className="flex flex-col"
|
|
900
|
-
>
|
|
901
|
-
{children}
|
|
902
|
-
</div>
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
### SEO Helper
|
|
123
|
+
For client-side GraphQL with Apollo:
|
|
908
124
|
|
|
909
125
|
```typescript
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
description: "Default description",
|
|
913
|
-
siteName: "MyApp",
|
|
914
|
-
themeColor: "#000000",
|
|
915
|
-
});
|
|
126
|
+
// gql/client.ts
|
|
127
|
+
import { getApolloWrapper } from "naystack/graphql/client";
|
|
916
128
|
|
|
917
|
-
|
|
918
|
-
export const metadata = getSEO(
|
|
919
|
-
"Page Title",
|
|
920
|
-
"Page description",
|
|
921
|
-
"/og-image.png"
|
|
922
|
-
);
|
|
129
|
+
export const ApolloWrapper = getApolloWrapper("/api");
|
|
923
130
|
```
|
|
924
131
|
|
|
925
|
-
###
|
|
926
|
-
|
|
927
|
-
Generate Instagram OAuth authorization URLs for client-side redirects.
|
|
928
|
-
|
|
929
|
-
**Example:**
|
|
132
|
+
### Frontend Usage
|
|
930
133
|
|
|
931
134
|
```typescript
|
|
932
|
-
import {
|
|
933
|
-
|
|
934
|
-
// Setup once
|
|
935
|
-
const getInstagramAuthorizationURL = getInstagramAuthorizationURLSetup(
|
|
936
|
-
process.env.NEXT_PUBLIC_INSTAGRAM_CLIENT_ID!,
|
|
937
|
-
`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/instagram`,
|
|
938
|
-
);
|
|
939
|
-
|
|
940
|
-
// Usage in component
|
|
941
|
-
function ConnectInstagramButton() {
|
|
942
|
-
const handleConnect = () => {
|
|
943
|
-
const authURL = getInstagramAuthorizationURL(userToken);
|
|
944
|
-
window.location.href = authURL;
|
|
945
|
-
};
|
|
135
|
+
import { useQuery, useMutation } from "@apollo/client";
|
|
946
136
|
|
|
947
|
-
|
|
137
|
+
function Profile() {
|
|
138
|
+
const { data } = useQuery(GetCurrentUserDocument);
|
|
139
|
+
// ...
|
|
948
140
|
}
|
|
949
141
|
```
|
|
950
142
|
|
|
951
|
-
|
|
143
|
+
---
|
|
952
144
|
|
|
953
|
-
|
|
954
|
-
const uploadImage = getHandleImageUpload("/api/upload");
|
|
955
|
-
|
|
956
|
-
const result = await uploadImage({
|
|
957
|
-
file: imageFile,
|
|
958
|
-
token: authToken,
|
|
959
|
-
type: "avatar",
|
|
960
|
-
data: { userId: 123 }, // Optional additional data
|
|
961
|
-
sync: true, // Optional: wait for processing
|
|
962
|
-
});
|
|
963
|
-
```
|
|
145
|
+
## 3. File Upload
|
|
964
146
|
|
|
965
|
-
|
|
147
|
+
Naystack simplifies AWS S3 file uploads with presigned URLs and client-side helpers.
|
|
966
148
|
|
|
967
|
-
|
|
149
|
+
### Server Setup
|
|
968
150
|
|
|
969
151
|
```typescript
|
|
970
152
|
import { setupFileUpload } from "naystack/file";
|
|
971
|
-
```
|
|
972
|
-
|
|
973
|
-
### Setup File Upload
|
|
974
153
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const fileUpload = setupFileUpload({
|
|
979
|
-
refreshKey: process.env.JWT_REFRESH_KEY!,
|
|
980
|
-
signingKey: process.env.JWT_SIGNING_KEY!,
|
|
981
|
-
region: "us-east-1",
|
|
982
|
-
bucket: "my-bucket",
|
|
154
|
+
export const { PUT, uploadFile, getDownloadURL } = setupFileUpload({
|
|
155
|
+
region: process.env.AWS_REGION!,
|
|
156
|
+
bucket: process.env.AWS_BUCKET!,
|
|
983
157
|
awsKey: process.env.AWS_ACCESS_KEY_ID!,
|
|
984
158
|
awsSecret: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
159
|
+
keys: {
|
|
160
|
+
signing: process.env.SIGNING_KEY!,
|
|
161
|
+
refresh: process.env.REFRESH_KEY!,
|
|
162
|
+
},
|
|
163
|
+
onUpload: async ({ url, type, userId, data }) => {
|
|
164
|
+
// Save info to DB or process after successful upload
|
|
165
|
+
return { success: true };
|
|
991
166
|
},
|
|
992
167
|
});
|
|
993
|
-
|
|
994
|
-
// Export route handler
|
|
995
|
-
export const { PUT } = fileUpload;
|
|
996
|
-
|
|
997
|
-
// Server-side utilities
|
|
998
|
-
const { getUploadFileURL, uploadImage, deleteImage, getFileURL, uploadFile } =
|
|
999
|
-
fileUpload;
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
**Real-World Example with Multiple File Types:**
|
|
1003
|
-
|
|
1004
|
-
```typescript
|
|
1005
|
-
import { setupFileUpload } from "naystack/file";
|
|
1006
|
-
import { db } from "@/lib/db";
|
|
1007
|
-
import { PortfolioTable, UserTable } from "@/lib/db/schema";
|
|
1008
|
-
import { and, eq } from "drizzle-orm";
|
|
1009
|
-
import { waitUntil } from "@vercel/functions";
|
|
1010
|
-
|
|
1011
|
-
export const { deleteImage, getUploadFileURL, getFileURL, uploadImage, PUT } =
|
|
1012
|
-
setupFileUpload({
|
|
1013
|
-
region: process.env.SITE_AWS_REGION!,
|
|
1014
|
-
refreshKey: process.env.REFRESH_KEY!,
|
|
1015
|
-
awsSecret: process.env.SITE_AWS_SECRET_ACCESS_KEY!,
|
|
1016
|
-
awsKey: process.env.SITE_AWS_ACCESS_KEY_ID!,
|
|
1017
|
-
signingKey: process.env.SIGNING_KEY!,
|
|
1018
|
-
bucket: process.env.SITE_AWS_BUCKET!,
|
|
1019
|
-
|
|
1020
|
-
processFile: async ({ url, userId, data, type }) => {
|
|
1021
|
-
switch (type) {
|
|
1022
|
-
case "PORTFOLIO":
|
|
1023
|
-
// Handle portfolio image upload
|
|
1024
|
-
waitUntil(
|
|
1025
|
-
(async () => {
|
|
1026
|
-
if (!url) return {};
|
|
1027
|
-
const id = (data as { id?: number }).id;
|
|
1028
|
-
|
|
1029
|
-
if (id) {
|
|
1030
|
-
// Update existing portfolio
|
|
1031
|
-
const [existing] = await db
|
|
1032
|
-
.select()
|
|
1033
|
-
.from(PortfolioTable)
|
|
1034
|
-
.where(
|
|
1035
|
-
and(
|
|
1036
|
-
eq(PortfolioTable.id, id),
|
|
1037
|
-
eq(PortfolioTable.user, userId),
|
|
1038
|
-
),
|
|
1039
|
-
);
|
|
1040
|
-
|
|
1041
|
-
if (!existing) {
|
|
1042
|
-
return { deleteURL: url };
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
const oldURL = existing.imageURL;
|
|
1046
|
-
await db
|
|
1047
|
-
.update(PortfolioTable)
|
|
1048
|
-
.set({ imageURL: url })
|
|
1049
|
-
.where(
|
|
1050
|
-
and(
|
|
1051
|
-
eq(PortfolioTable.id, id),
|
|
1052
|
-
eq(PortfolioTable.user, userId),
|
|
1053
|
-
),
|
|
1054
|
-
);
|
|
1055
|
-
|
|
1056
|
-
return {
|
|
1057
|
-
deleteURL: oldURL,
|
|
1058
|
-
data: { id },
|
|
1059
|
-
};
|
|
1060
|
-
} else {
|
|
1061
|
-
// Create new portfolio
|
|
1062
|
-
const [portfolio] = await db
|
|
1063
|
-
.insert(PortfolioTable)
|
|
1064
|
-
.values({
|
|
1065
|
-
user: userId,
|
|
1066
|
-
imageURL: url,
|
|
1067
|
-
caption: "",
|
|
1068
|
-
link: "",
|
|
1069
|
-
})
|
|
1070
|
-
.returning({ id: PortfolioTable.id });
|
|
1071
|
-
|
|
1072
|
-
return { data: { id: portfolio?.id } };
|
|
1073
|
-
}
|
|
1074
|
-
})(),
|
|
1075
|
-
);
|
|
1076
|
-
break;
|
|
1077
|
-
|
|
1078
|
-
case "PROFILE_PICTURE":
|
|
1079
|
-
// Handle profile picture upload
|
|
1080
|
-
waitUntil(
|
|
1081
|
-
(async () => {
|
|
1082
|
-
const user = await db.query.UserTable.findFirst({
|
|
1083
|
-
where: eq(UserTable.id, userId),
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
if (!user && url) {
|
|
1087
|
-
return { deleteURL: url };
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
const oldPhoto = user?.photo;
|
|
1091
|
-
await db
|
|
1092
|
-
.update(UserTable)
|
|
1093
|
-
.set({ photo: url })
|
|
1094
|
-
.where(eq(UserTable.id, userId));
|
|
1095
|
-
|
|
1096
|
-
return {
|
|
1097
|
-
deleteURL: oldPhoto || undefined,
|
|
1098
|
-
};
|
|
1099
|
-
})(),
|
|
1100
|
-
);
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
return {};
|
|
1105
|
-
},
|
|
1106
|
-
});
|
|
1107
|
-
```
|
|
1108
|
-
|
|
1109
|
-
#### Options
|
|
1110
|
-
|
|
1111
|
-
| Option | Type | Required | Description |
|
|
1112
|
-
| ------------- | ---------- | -------- | ------------------------ |
|
|
1113
|
-
| `refreshKey` | `string` | ✅ | JWT refresh key |
|
|
1114
|
-
| `signingKey` | `string` | ✅ | JWT signing key |
|
|
1115
|
-
| `region` | `string` | ✅ | AWS S3 region |
|
|
1116
|
-
| `bucket` | `string` | ✅ | AWS S3 bucket name |
|
|
1117
|
-
| `awsKey` | `string` | ✅ | AWS access key ID |
|
|
1118
|
-
| `awsSecret` | `string` | ✅ | AWS secret access key |
|
|
1119
|
-
| `processFile` | `Function` | ✅ | File processing callback |
|
|
1120
|
-
|
|
1121
|
-
#### Returned Utilities
|
|
1122
|
-
|
|
1123
|
-
| Utility | Description |
|
|
1124
|
-
| ------------------ | ------------------------------ |
|
|
1125
|
-
| `PUT` | Route handler for file uploads |
|
|
1126
|
-
| `getUploadFileURL` | Get presigned URL for upload |
|
|
1127
|
-
| `uploadImage` | Upload image to S3 |
|
|
1128
|
-
| `deleteImage` | Delete image from S3 |
|
|
1129
|
-
| `getFileURL` | Get public URL for a file |
|
|
1130
|
-
| `uploadFile` | Upload any file to S3 |
|
|
1131
|
-
|
|
1132
|
-
---
|
|
1133
|
-
|
|
1134
|
-
## Socials Module
|
|
1135
|
-
|
|
1136
|
-
```typescript
|
|
1137
|
-
import {
|
|
1138
|
-
// Instagram
|
|
1139
|
-
getInstagramUser,
|
|
1140
|
-
getInstagramMedia,
|
|
1141
|
-
getInstagramConversations,
|
|
1142
|
-
getInstagramConversationsByUser,
|
|
1143
|
-
getInstagramConversationByUser,
|
|
1144
|
-
getInstagramConversation,
|
|
1145
|
-
getInstagramMessage,
|
|
1146
|
-
sendInstagramMessage,
|
|
1147
|
-
setupInstagramWebhook,
|
|
1148
|
-
// Threads
|
|
1149
|
-
getThread,
|
|
1150
|
-
getThreads,
|
|
1151
|
-
getThreadsReplies,
|
|
1152
|
-
createThread,
|
|
1153
|
-
createThreadsPost,
|
|
1154
|
-
setupThreadsWebhook,
|
|
1155
|
-
} from "naystack/socials";
|
|
1156
168
|
```
|
|
1157
169
|
|
|
1158
|
-
###
|
|
1159
|
-
|
|
1160
|
-
#### Get User Data
|
|
170
|
+
### Client Setup & Usage
|
|
1161
171
|
|
|
1162
172
|
```typescript
|
|
1163
|
-
|
|
1164
|
-
const user = await getInstagramUser(accessToken, "user_id");
|
|
1165
|
-
const user = await getInstagramUser(accessToken, "me", [
|
|
1166
|
-
"username",
|
|
1167
|
-
"followers_count",
|
|
1168
|
-
]);
|
|
1169
|
-
```
|
|
173
|
+
import { getUseFileUpload } from "naystack/file/client";
|
|
1170
174
|
|
|
1171
|
-
|
|
175
|
+
const useFileUpload = getUseFileUpload("/api/upload");
|
|
1172
176
|
|
|
1173
|
-
|
|
177
|
+
function UploadButton() {
|
|
178
|
+
const upload = useFileUpload();
|
|
1174
179
|
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
24
|
|
1181
|
-
);
|
|
1182
|
-
```
|
|
1183
|
-
|
|
1184
|
-
**Real-World Example - Fetching Media with Custom Fields:**
|
|
1185
|
-
|
|
1186
|
-
```typescript
|
|
1187
|
-
import { getInstagramMedia } from "naystack/socials";
|
|
1188
|
-
|
|
1189
|
-
export async function fetchInstagramGraphMedia(
|
|
1190
|
-
accessToken: string,
|
|
1191
|
-
followers: number,
|
|
1192
|
-
userId: number,
|
|
1193
|
-
) {
|
|
1194
|
-
const fetchReq = await getInstagramMedia<{
|
|
1195
|
-
thumbnail_url?: string;
|
|
1196
|
-
id: string;
|
|
1197
|
-
like_count?: number;
|
|
1198
|
-
comments_count: number;
|
|
1199
|
-
permalink: string;
|
|
1200
|
-
caption: string;
|
|
1201
|
-
media_url?: string;
|
|
1202
|
-
media_type?: string;
|
|
1203
|
-
timestamp: string;
|
|
1204
|
-
}>(accessToken, [
|
|
1205
|
-
"id",
|
|
1206
|
-
"thumbnail_url",
|
|
1207
|
-
"media_url",
|
|
1208
|
-
"like_count",
|
|
1209
|
-
"comments_count",
|
|
1210
|
-
"media_type",
|
|
1211
|
-
"permalink",
|
|
1212
|
-
"caption",
|
|
1213
|
-
"timestamp",
|
|
1214
|
-
]);
|
|
1215
|
-
|
|
1216
|
-
if (fetchReq?.data) {
|
|
1217
|
-
return fetchReq.data.map((media) => ({
|
|
1218
|
-
isVideo: media.media_type === "VIDEO",
|
|
1219
|
-
comments: media.comments_count || -1,
|
|
1220
|
-
likes: media.like_count || 0,
|
|
1221
|
-
link: media.permalink,
|
|
1222
|
-
thumbnail: media.thumbnail_url || media.media_url,
|
|
1223
|
-
mediaURL: media.media_url,
|
|
1224
|
-
timestamp: media.timestamp,
|
|
1225
|
-
caption: media.caption,
|
|
1226
|
-
appID: media.id,
|
|
1227
|
-
user: userId,
|
|
1228
|
-
er: calculateEngagementRate(
|
|
1229
|
-
followers,
|
|
1230
|
-
media.like_count || 0,
|
|
1231
|
-
media.comments_count || -1,
|
|
1232
|
-
),
|
|
1233
|
-
}));
|
|
1234
|
-
}
|
|
180
|
+
const handleFile = async (file: File) => {
|
|
181
|
+
const res = await upload(file, "profile-picture", { some: "metadata" });
|
|
182
|
+
console.log("Uploaded URL:", res.url);
|
|
183
|
+
};
|
|
184
|
+
// ...
|
|
1235
185
|
}
|
|
1236
186
|
```
|
|
1237
187
|
|
|
1238
|
-
#### Conversations
|
|
1239
|
-
|
|
1240
|
-
```typescript
|
|
1241
|
-
// Get all conversations
|
|
1242
|
-
const { data, fetchMore } = await getInstagramConversations(accessToken);
|
|
1243
|
-
|
|
1244
|
-
// Get conversations by user
|
|
1245
|
-
const conversations = await getInstagramConversationsByUser(
|
|
1246
|
-
accessToken,
|
|
1247
|
-
userId
|
|
1248
|
-
);
|
|
1249
|
-
|
|
1250
|
-
// Get single conversation
|
|
1251
|
-
const conversation = await getInstagramConversationByUser(accessToken, userId);
|
|
1252
|
-
|
|
1253
|
-
// Get conversation with messages
|
|
1254
|
-
const { messages, participants, fetchMore } = await getInstagramConversation(
|
|
1255
|
-
accessToken,
|
|
1256
|
-
conversationId
|
|
1257
|
-
);
|
|
1258
|
-
```
|
|
1259
|
-
|
|
1260
|
-
#### Messages
|
|
1261
|
-
|
|
1262
|
-
```typescript
|
|
1263
|
-
// Get message details
|
|
1264
|
-
const message = await getInstagramMessage(accessToken, messageId);
|
|
1265
|
-
|
|
1266
|
-
// Send message
|
|
1267
|
-
const result = await sendInstagramMessage(accessToken, recipientId, "Hello!");
|
|
1268
|
-
```
|
|
1269
|
-
|
|
1270
|
-
#### Webhook
|
|
1271
|
-
|
|
1272
|
-
**Basic Example:**
|
|
1273
|
-
|
|
1274
|
-
```typescript
|
|
1275
|
-
const instagramWebhook = setupInstagramWebhook({
|
|
1276
|
-
secret: process.env.INSTAGRAM_WEBHOOK_SECRET!,
|
|
1277
|
-
callback: async (type, value, id) => {
|
|
1278
|
-
// Handle webhook events
|
|
1279
|
-
},
|
|
1280
|
-
});
|
|
1281
|
-
|
|
1282
|
-
export const { GET, POST } = instagramWebhook;
|
|
1283
|
-
```
|
|
1284
|
-
|
|
1285
|
-
**Real-World Example - Auto-Reply Bot:**
|
|
1286
|
-
|
|
1287
|
-
```typescript
|
|
1288
|
-
import {
|
|
1289
|
-
getInstagramConversationByUser,
|
|
1290
|
-
sendInstagramMessage,
|
|
1291
|
-
setupInstagramWebhook,
|
|
1292
|
-
} from "naystack/socials";
|
|
1293
|
-
|
|
1294
|
-
export const { GET, POST } = setupInstagramWebhook({
|
|
1295
|
-
secret: process.env.REFRESH_KEY!,
|
|
1296
|
-
callback: async (
|
|
1297
|
-
type,
|
|
1298
|
-
value: {
|
|
1299
|
-
sender: { id: string };
|
|
1300
|
-
message: { text: string };
|
|
1301
|
-
recipient: { id: string };
|
|
1302
|
-
},
|
|
1303
|
-
) => {
|
|
1304
|
-
if (
|
|
1305
|
-
type === "messaging" &&
|
|
1306
|
-
value.message.text &&
|
|
1307
|
-
value.sender.id !== "YOUR_PAGE_ID" &&
|
|
1308
|
-
value.recipient.id === "YOUR_PAGE_ID"
|
|
1309
|
-
) {
|
|
1310
|
-
// Check if message is recent (within 24 hours)
|
|
1311
|
-
const conversation = await getInstagramConversationByUser(
|
|
1312
|
-
process.env.INSTAGRAM_ACCESS_TOKEN!,
|
|
1313
|
-
value.sender.id,
|
|
1314
|
-
);
|
|
1315
|
-
|
|
1316
|
-
const lastMessage = conversation?.messages?.data[1]?.created_time;
|
|
1317
|
-
if (lastMessage) {
|
|
1318
|
-
const lastMessageDate = new Date(lastMessage);
|
|
1319
|
-
if (lastMessageDate.getTime() > Date.now() - 1000 * 60 * 60 * 24) {
|
|
1320
|
-
return; // Already replied recently
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
// Generate reply (using your AI/LLM service)
|
|
1325
|
-
const reply = await generateReply(value.message.text);
|
|
1326
|
-
if (reply && reply !== '""') {
|
|
1327
|
-
await sendInstagramMessage(
|
|
1328
|
-
process.env.INSTAGRAM_ACCESS_TOKEN!,
|
|
1329
|
-
value.sender.id,
|
|
1330
|
-
reply,
|
|
1331
|
-
);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
},
|
|
1335
|
-
});
|
|
1336
|
-
```
|
|
1337
|
-
|
|
1338
188
|
---
|
|
1339
189
|
|
|
1340
|
-
|
|
190
|
+
## 4. Other Utilities
|
|
1341
191
|
|
|
1342
|
-
|
|
192
|
+
### Client Hooks
|
|
1343
193
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
const thread = await getThread(accessToken, threadId);
|
|
1347
|
-
const thread = await getThread(accessToken, threadId, ["text", "permalink"]);
|
|
1348
|
-
|
|
1349
|
-
// Get all threads
|
|
1350
|
-
const threads = await getThreads(accessToken);
|
|
194
|
+
- `useVisibility(onVisible)`: Triggers a callback when an element enters the viewport.
|
|
195
|
+
- `useBreakpoint(query)`: Responsive media query hook.
|
|
1351
196
|
|
|
1352
|
-
|
|
1353
|
-
const replies = await getThreadsReplies(accessToken, threadId);
|
|
1354
|
-
```
|
|
197
|
+
### SEO
|
|
1355
198
|
|
|
1356
|
-
|
|
199
|
+
The `setupSEO` utility helps generate optimized Next.js metadata.
|
|
1357
200
|
|
|
1358
201
|
```typescript
|
|
1359
|
-
|
|
1360
|
-
const postId = await createThreadsPost(accessToken, "Hello, Threads!");
|
|
1361
|
-
|
|
1362
|
-
// Reply to a thread
|
|
1363
|
-
const replyId = await createThreadsPost(
|
|
1364
|
-
accessToken,
|
|
1365
|
-
"Reply text",
|
|
1366
|
-
parentThreadId
|
|
1367
|
-
);
|
|
1368
|
-
|
|
1369
|
-
// Create threaded posts (carousel)
|
|
1370
|
-
const firstPostId = await createThread(accessToken, [
|
|
1371
|
-
"First post",
|
|
1372
|
-
"Second post in thread",
|
|
1373
|
-
"Third post in thread",
|
|
1374
|
-
]);
|
|
1375
|
-
```
|
|
1376
|
-
|
|
1377
|
-
#### Webhook
|
|
202
|
+
import { setupSEO } from "naystack/client";
|
|
1378
203
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
callback: async (type, value) => {
|
|
1385
|
-
// Handle webhook events
|
|
1386
|
-
return true; // Return false to indicate failure
|
|
1387
|
-
},
|
|
204
|
+
export const getMetadata = setupSEO({
|
|
205
|
+
siteName: "Naystack",
|
|
206
|
+
title: "A powerful stack",
|
|
207
|
+
description: "Built with Next.js",
|
|
208
|
+
themeColor: "#000000",
|
|
1388
209
|
});
|
|
1389
|
-
|
|
1390
|
-
export const { GET, POST } = threadsWebhook;
|
|
1391
210
|
```
|
|
1392
211
|
|
|
1393
|
-
|
|
212
|
+
### Social APIs
|
|
1394
213
|
|
|
1395
|
-
|
|
1396
|
-
import {
|
|
1397
|
-
createThreadsPost,
|
|
1398
|
-
getThreadsReplies,
|
|
1399
|
-
setupThreadsWebhook,
|
|
1400
|
-
} from "naystack/socials";
|
|
1401
|
-
import { db } from "@/lib/db";
|
|
1402
|
-
import { SocialPostsTable } from "@/lib/db/schema";
|
|
1403
|
-
import { eq } from "drizzle-orm";
|
|
214
|
+
Simplified access to Instagram and Threads APIs.
|
|
1404
215
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
"We'd love to work with you! See campaign ${ID}",
|
|
1408
|
-
];
|
|
1409
|
-
|
|
1410
|
-
export const { GET, POST } = setupThreadsWebhook({
|
|
1411
|
-
secret: process.env.REFRESH_KEY!,
|
|
1412
|
-
callback: async (
|
|
1413
|
-
field,
|
|
1414
|
-
value: {
|
|
1415
|
-
id: string;
|
|
1416
|
-
username: string;
|
|
1417
|
-
text: string;
|
|
1418
|
-
replied_to: { id: string };
|
|
1419
|
-
root_post: { id: string; username: string };
|
|
1420
|
-
},
|
|
1421
|
-
) => {
|
|
1422
|
-
// Skip if replying to own post
|
|
1423
|
-
if (value.root_post.username === value.username) return true;
|
|
1424
|
-
|
|
1425
|
-
// Only reply to direct replies to root post
|
|
1426
|
-
if (value.root_post.id !== value.replied_to.id) return true;
|
|
1427
|
-
|
|
1428
|
-
// Check if already replied
|
|
1429
|
-
const replies = await getThreadsReplies(
|
|
1430
|
-
process.env.THREADS_ACCESS_TOKEN!,
|
|
1431
|
-
value.id,
|
|
1432
|
-
);
|
|
1433
|
-
if (replies?.length ?? 0 > 0) return true;
|
|
1434
|
-
|
|
1435
|
-
// Find associated campaign
|
|
1436
|
-
const [post] = await db
|
|
1437
|
-
.select()
|
|
1438
|
-
.from(SocialPostsTable)
|
|
1439
|
-
.where(eq(SocialPostsTable.postID, value.root_post.id));
|
|
1440
|
-
|
|
1441
|
-
if (!post) return true;
|
|
1442
|
-
|
|
1443
|
-
// Generate reply message
|
|
1444
|
-
const message = REPLIES[
|
|
1445
|
-
Math.floor(Math.random() * REPLIES.length)
|
|
1446
|
-
]?.replace("${ID}", post.campaignID.toString());
|
|
1447
|
-
|
|
1448
|
-
if (!message) return true;
|
|
1449
|
-
|
|
1450
|
-
// Send reply
|
|
1451
|
-
const res = await createThreadsPost(
|
|
1452
|
-
process.env.THREADS_ACCESS_TOKEN!,
|
|
1453
|
-
message,
|
|
1454
|
-
value.id,
|
|
1455
|
-
);
|
|
1456
|
-
|
|
1457
|
-
return !!res;
|
|
1458
|
-
},
|
|
1459
|
-
});
|
|
216
|
+
```typescript
|
|
217
|
+
import { getInstagramUser, createThreadsPost } from "naystack/socials";
|
|
1460
218
|
```
|
|
1461
219
|
|
|
1462
220
|
---
|
|
1463
221
|
|
|
1464
|
-
##
|
|
1465
|
-
|
|
1466
|
-
### Authentication Flow
|
|
1467
|
-
|
|
1468
|
-
1. **Email Auth with Database Integration:**
|
|
1469
|
-
- Use Drizzle ORM queries in `getUser` and `createUser`
|
|
1470
|
-
- Send verification emails in `onSignUp` callback
|
|
1471
|
-
- Clean up session data in `onLogout`
|
|
1472
|
-
|
|
1473
|
-
2. **OAuth Integration:**
|
|
1474
|
-
- Always verify email/username matches existing accounts
|
|
1475
|
-
- Create new users only when necessary
|
|
1476
|
-
- Update verification status for existing users
|
|
1477
|
-
|
|
1478
|
-
### GraphQL Resolver Patterns
|
|
1479
|
-
|
|
1480
|
-
1. **Error Handling:**
|
|
1481
|
-
- Use `GQLError` for all error cases
|
|
1482
|
-
- Provide clear, user-friendly error messages
|
|
1483
|
-
- Use appropriate HTTP status codes (400, 403, 404, 500)
|
|
1484
|
-
|
|
1485
|
-
2. **Authorization:**
|
|
1486
|
-
- Check authentication first (`if (!ctx.userId)`)
|
|
1487
|
-
- Verify permissions before operations
|
|
1488
|
-
- Use `authChecker` for role-based access control
|
|
1489
|
-
|
|
1490
|
-
3. **Query Library Pattern:**
|
|
1491
|
-
- Use `query()` helper for simple queries/mutations
|
|
1492
|
-
- Use `QueryLibrary()` to combine multiple queries
|
|
1493
|
-
- Use `field()` and `FieldLibrary()` for computed fields
|
|
1494
|
-
|
|
1495
|
-
### File Upload Patterns
|
|
1496
|
-
|
|
1497
|
-
1. **Multiple File Types:**
|
|
1498
|
-
- Use `type` parameter to distinguish upload types
|
|
1499
|
-
- Handle each type in `processFile` callback
|
|
1500
|
-
- Return `deleteURL` for old files to clean up
|
|
1501
|
-
|
|
1502
|
-
2. **Async Processing:**
|
|
1503
|
-
- Use `waitUntil()` for non-blocking operations
|
|
1504
|
-
- Process file metadata in background
|
|
1505
|
-
- Update database after successful upload
|
|
1506
|
-
|
|
1507
|
-
### Webhook Patterns
|
|
1508
|
-
|
|
1509
|
-
1. **Instagram Webhooks:**
|
|
1510
|
-
- Verify sender/recipient IDs
|
|
1511
|
-
- Check message timestamps to avoid duplicates
|
|
1512
|
-
- Use async operations for replies
|
|
1513
|
-
|
|
1514
|
-
2. **Threads Webhooks:**
|
|
1515
|
-
- Filter by reply structure (root_post vs replied_to)
|
|
1516
|
-
- Check for existing replies before responding
|
|
1517
|
-
- Return `true`/`false` to indicate success/failure
|
|
1518
|
-
|
|
1519
|
-
### Client-Side Patterns
|
|
1520
|
-
|
|
1521
|
-
1. **Infinite Scroll:**
|
|
1522
|
-
- Use `useVisibility` hook with `fetchMore` callback
|
|
1523
|
-
- Attach ref to last item in list
|
|
1524
|
-
- Handle loading states
|
|
1525
|
-
|
|
1526
|
-
2. **Responsive Design:**
|
|
1527
|
-
- Use `useBreakpoint` for conditional rendering
|
|
1528
|
-
- Adjust layout based on screen size
|
|
1529
|
-
- Combine with CSS for optimal UX
|
|
1530
|
-
|
|
1531
|
-
---
|
|
1532
|
-
|
|
1533
|
-
## Environment Variables
|
|
1534
|
-
|
|
1535
|
-
Here's a summary of common environment variables used:
|
|
222
|
+
## Minimal Environment Variables
|
|
1536
223
|
|
|
1537
224
|
```env
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
# Instagram
|
|
1547
|
-
INSTAGRAM_CLIENT_ID=your-instagram-client-id
|
|
1548
|
-
INSTAGRAM_CLIENT_SECRET=your-instagram-client-secret
|
|
1549
|
-
INSTAGRAM_WEBHOOK_SECRET=your-instagram-webhook-secret
|
|
1550
|
-
|
|
1551
|
-
# Threads
|
|
1552
|
-
THREADS_WEBHOOK_SECRET=your-threads-webhook-secret
|
|
1553
|
-
|
|
1554
|
-
# AWS S3
|
|
1555
|
-
AWS_ACCESS_KEY_ID=your-aws-key
|
|
1556
|
-
AWS_SECRET_ACCESS_KEY=your-aws-secret
|
|
1557
|
-
|
|
1558
|
-
# Cloudflare Turnstile (optional)
|
|
1559
|
-
TURNSTILE_SECRET_KEY=your-turnstile-key
|
|
1560
|
-
|
|
1561
|
-
# App
|
|
1562
|
-
NEXT_PUBLIC_BASE_URL=https://your-app.com
|
|
225
|
+
SIGNING_KEY=your-jwt-signing-key
|
|
226
|
+
REFRESH_KEY=your-jwt-refresh-key
|
|
227
|
+
NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:3000/api
|
|
228
|
+
AWS_REGION=us-east-1
|
|
229
|
+
AWS_BUCKET=your-bucket-name
|
|
230
|
+
AWS_ACCESS_KEY_ID=xxx
|
|
231
|
+
AWS_SECRET_ACCESS_KEY=xxx
|
|
1563
232
|
```
|
|
1564
|
-
|
|
1565
|
-
---
|
|
1566
|
-
|
|
1567
|
-
## License
|
|
1568
|
-
|
|
1569
|
-
ISC © [Abhinay Pandey](mailto:abhinaypandey02@gmail.com)
|