create-n8-app 0.1.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/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +239 -0
- package/package.json +58 -0
- package/template/_env.example +34 -0
- package/template/_env.local +31 -0
- package/template/_eslintrc.json +3 -0
- package/template/_gitignore +46 -0
- package/template/_package.json +59 -0
- package/template/app/api/auth/[...nextauth]/route.ts +3 -0
- package/template/app/api/trpc/[trpc]/route.ts +19 -0
- package/template/app/auth/signin/page.tsx +39 -0
- package/template/app/globals.css +33 -0
- package/template/app/layout.tsx +28 -0
- package/template/app/page.tsx +68 -0
- package/template/app/providers.tsx +47 -0
- package/template/components/auth/user-button.tsx +46 -0
- package/template/components/ui/.gitkeep +2 -0
- package/template/components.json +21 -0
- package/template/drizzle/.gitkeep +1 -0
- package/template/drizzle.config.ts +10 -0
- package/template/hooks/.gitkeep +1 -0
- package/template/lib/auth.ts +44 -0
- package/template/lib/db.ts +10 -0
- package/template/lib/env.ts +76 -0
- package/template/lib/trpc.ts +4 -0
- package/template/lib/utils.ts +6 -0
- package/template/next-env.d.ts +5 -0
- package/template/next.config.ts +14 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prettier.config.js +11 -0
- package/template/public/.gitkeep +1 -0
- package/template/server/api/root.ts +22 -0
- package/template/server/api/routers/example.ts +76 -0
- package/template/server/api/trpc.ts +67 -0
- package/template/server/db/schema.ts +95 -0
- package/template/stores/example-store.ts +121 -0
- package/template/tests/example.test.ts +22 -0
- package/template/tests/setup.ts +22 -0
- package/template/tsconfig.json +27 -0
- package/template/vitest.config.ts +30 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { signIn } from '@/lib/auth'
|
|
2
|
+
|
|
3
|
+
export default function SignInPage() {
|
|
4
|
+
return (
|
|
5
|
+
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-slate-900 to-slate-800">
|
|
6
|
+
<div className="flex flex-col items-center gap-8 rounded-xl bg-white/10 p-8">
|
|
7
|
+
<h1 className="text-3xl font-bold text-white">Sign In</h1>
|
|
8
|
+
<p className="text-slate-300">Sign in to access your account</p>
|
|
9
|
+
|
|
10
|
+
<form
|
|
11
|
+
action={async () => {
|
|
12
|
+
'use server'
|
|
13
|
+
await signIn('github', { redirectTo: '/' })
|
|
14
|
+
}}
|
|
15
|
+
>
|
|
16
|
+
<button
|
|
17
|
+
type="submit"
|
|
18
|
+
className="flex items-center gap-3 rounded-lg bg-slate-800 px-6 py-3 text-white transition-colors hover:bg-slate-700"
|
|
19
|
+
>
|
|
20
|
+
<GitHubIcon />
|
|
21
|
+
Sign in with GitHub
|
|
22
|
+
</button>
|
|
23
|
+
</form>
|
|
24
|
+
</div>
|
|
25
|
+
</main>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function GitHubIcon() {
|
|
30
|
+
return (
|
|
31
|
+
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
|
32
|
+
<path
|
|
33
|
+
fillRule="evenodd"
|
|
34
|
+
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
|
35
|
+
clipRule="evenodd"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
/* Custom theme configuration */
|
|
5
|
+
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
|
6
|
+
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
7
|
+
|
|
8
|
+
/* Primary colors */
|
|
9
|
+
--color-primary-50: oklch(0.97 0.02 250);
|
|
10
|
+
--color-primary-100: oklch(0.93 0.04 250);
|
|
11
|
+
--color-primary-200: oklch(0.86 0.08 250);
|
|
12
|
+
--color-primary-300: oklch(0.76 0.12 250);
|
|
13
|
+
--color-primary-400: oklch(0.64 0.16 250);
|
|
14
|
+
--color-primary-500: oklch(0.55 0.18 250);
|
|
15
|
+
--color-primary-600: oklch(0.48 0.18 250);
|
|
16
|
+
--color-primary-700: oklch(0.42 0.16 250);
|
|
17
|
+
--color-primary-800: oklch(0.36 0.14 250);
|
|
18
|
+
--color-primary-900: oklch(0.30 0.10 250);
|
|
19
|
+
--color-primary-950: oklch(0.22 0.08 250);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@layer base {
|
|
23
|
+
* {
|
|
24
|
+
@apply border-border;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
@apply bg-background text-foreground;
|
|
29
|
+
font-feature-settings:
|
|
30
|
+
'rlig' 1,
|
|
31
|
+
'calt' 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { Inter } from 'next/font/google'
|
|
3
|
+
import './globals.css'
|
|
4
|
+
import { Providers } from './providers'
|
|
5
|
+
|
|
6
|
+
const inter = Inter({
|
|
7
|
+
subsets: ['latin'],
|
|
8
|
+
variable: '--font-sans',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const metadata: Metadata = {
|
|
12
|
+
title: 'N8 App',
|
|
13
|
+
description: 'Built with the N8 stack - Next.js, Tailwind, Shadcn/ui, Drizzle, tRPC, and more',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function RootLayout({
|
|
17
|
+
children,
|
|
18
|
+
}: Readonly<{
|
|
19
|
+
children: React.ReactNode
|
|
20
|
+
}>) {
|
|
21
|
+
return (
|
|
22
|
+
<html lang="en" suppressHydrationWarning>
|
|
23
|
+
<body className={`${inter.variable} font-sans antialiased`}>
|
|
24
|
+
<Providers>{children}</Providers>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
|
|
3
|
+
export default function Home() {
|
|
4
|
+
return (
|
|
5
|
+
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-slate-900 to-slate-800 text-white">
|
|
6
|
+
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
|
|
7
|
+
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
|
|
8
|
+
N8 <span className="text-primary-400">Stack</span>
|
|
9
|
+
</h1>
|
|
10
|
+
|
|
11
|
+
<p className="max-w-2xl text-center text-lg text-slate-300">
|
|
12
|
+
A modern, full-stack Next.js template with TypeScript, Tailwind CSS, Shadcn/ui, Drizzle
|
|
13
|
+
ORM, tRPC, TanStack Query, Zustand, and NextAuth.js.
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
|
|
17
|
+
<FeatureCard
|
|
18
|
+
title="Next.js 16"
|
|
19
|
+
description="App Router, Server Components, Server Actions, Turbopack"
|
|
20
|
+
href="https://nextjs.org/docs"
|
|
21
|
+
/>
|
|
22
|
+
<FeatureCard
|
|
23
|
+
title="Tailwind + Shadcn"
|
|
24
|
+
description="Modern styling with beautiful, accessible components"
|
|
25
|
+
href="https://ui.shadcn.com"
|
|
26
|
+
/>
|
|
27
|
+
<FeatureCard
|
|
28
|
+
title="Drizzle + Neon"
|
|
29
|
+
description="Type-safe ORM with serverless Postgres"
|
|
30
|
+
href="https://orm.drizzle.team"
|
|
31
|
+
/>
|
|
32
|
+
<FeatureCard
|
|
33
|
+
title="tRPC"
|
|
34
|
+
description="End-to-end type safety for your API"
|
|
35
|
+
href="https://trpc.io"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="flex flex-col items-center gap-2">
|
|
40
|
+
<p className="text-sm text-slate-400">Get started by editing</p>
|
|
41
|
+
<code className="rounded bg-slate-700 px-3 py-1 font-mono text-sm">app/page.tsx</code>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</main>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function FeatureCard({
|
|
49
|
+
title,
|
|
50
|
+
description,
|
|
51
|
+
href,
|
|
52
|
+
}: {
|
|
53
|
+
title: string
|
|
54
|
+
description: string
|
|
55
|
+
href: string
|
|
56
|
+
}) {
|
|
57
|
+
return (
|
|
58
|
+
<Link
|
|
59
|
+
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20 transition-colors"
|
|
60
|
+
href={href}
|
|
61
|
+
target="_blank"
|
|
62
|
+
rel="noopener noreferrer"
|
|
63
|
+
>
|
|
64
|
+
<h3 className="text-2xl font-bold">{title} →</h3>
|
|
65
|
+
<p className="text-slate-300">{description}</p>
|
|
66
|
+
</Link>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { httpBatchLink } from '@trpc/client'
|
|
5
|
+
import { SessionProvider } from 'next-auth/react'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
import { trpc } from '@/lib/trpc'
|
|
8
|
+
import superjson from 'superjson'
|
|
9
|
+
|
|
10
|
+
function getBaseUrl() {
|
|
11
|
+
if (typeof window !== 'undefined') return ''
|
|
12
|
+
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`
|
|
13
|
+
return `http://localhost:${process.env.PORT ?? 3000}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
17
|
+
const [queryClient] = useState(
|
|
18
|
+
() =>
|
|
19
|
+
new QueryClient({
|
|
20
|
+
defaultOptions: {
|
|
21
|
+
queries: {
|
|
22
|
+
staleTime: 5 * 1000,
|
|
23
|
+
refetchOnWindowFocus: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const [trpcClient] = useState(() =>
|
|
30
|
+
trpc.createClient({
|
|
31
|
+
links: [
|
|
32
|
+
httpBatchLink({
|
|
33
|
+
url: `${getBaseUrl()}/api/trpc`,
|
|
34
|
+
transformer: superjson,
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<SessionProvider>
|
|
42
|
+
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
43
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
44
|
+
</trpc.Provider>
|
|
45
|
+
</SessionProvider>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useSession, signIn, signOut } from 'next-auth/react'
|
|
4
|
+
import Image from 'next/image'
|
|
5
|
+
|
|
6
|
+
export function UserButton() {
|
|
7
|
+
const { data: session, status } = useSession()
|
|
8
|
+
|
|
9
|
+
if (status === 'loading') {
|
|
10
|
+
return (
|
|
11
|
+
<div className="h-8 w-8 animate-pulse rounded-full bg-slate-600" />
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!session) {
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
onClick={() => signIn('github')}
|
|
19
|
+
className="rounded-lg bg-white/10 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-white/20"
|
|
20
|
+
>
|
|
21
|
+
Sign In
|
|
22
|
+
</button>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex items-center gap-3">
|
|
28
|
+
{session.user.image && (
|
|
29
|
+
<Image
|
|
30
|
+
src={session.user.image}
|
|
31
|
+
alt={session.user.name ?? 'User'}
|
|
32
|
+
width={32}
|
|
33
|
+
height={32}
|
|
34
|
+
className="rounded-full"
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
<span className="text-sm text-white">{session.user.name}</span>
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => signOut()}
|
|
40
|
+
className="rounded-lg bg-white/10 px-3 py-1 text-sm text-white transition-colors hover:bg-white/20"
|
|
41
|
+
>
|
|
42
|
+
Sign Out
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "slate",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/hooks"
|
|
19
|
+
},
|
|
20
|
+
"iconLibrary": "lucide"
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Drizzle migrations will be generated here
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Custom React hooks go here
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import NextAuth from 'next-auth'
|
|
2
|
+
import GitHub from 'next-auth/providers/github'
|
|
3
|
+
import { DrizzleAdapter } from '@auth/drizzle-adapter'
|
|
4
|
+
import { db } from '@/lib/db'
|
|
5
|
+
import { accounts, sessions, users, verificationTokens } from '@/server/db/schema'
|
|
6
|
+
|
|
7
|
+
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
8
|
+
adapter: DrizzleAdapter(db, {
|
|
9
|
+
usersTable: users,
|
|
10
|
+
accountsTable: accounts,
|
|
11
|
+
sessionsTable: sessions,
|
|
12
|
+
verificationTokensTable: verificationTokens,
|
|
13
|
+
}),
|
|
14
|
+
providers: [
|
|
15
|
+
GitHub({
|
|
16
|
+
clientId: process.env.AUTH_GITHUB_ID,
|
|
17
|
+
clientSecret: process.env.AUTH_GITHUB_SECRET,
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
callbacks: {
|
|
21
|
+
session: ({ session, user }) => ({
|
|
22
|
+
...session,
|
|
23
|
+
user: {
|
|
24
|
+
...session.user,
|
|
25
|
+
id: user.id,
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
pages: {
|
|
30
|
+
signIn: '/auth/signin',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Extend the built-in session types
|
|
35
|
+
declare module 'next-auth' {
|
|
36
|
+
interface Session {
|
|
37
|
+
user: {
|
|
38
|
+
id: string
|
|
39
|
+
name?: string | null
|
|
40
|
+
email?: string | null
|
|
41
|
+
image?: string | null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { neon } from '@neondatabase/serverless'
|
|
2
|
+
import { drizzle } from 'drizzle-orm/neon-http'
|
|
3
|
+
import * as schema from '@/server/db/schema'
|
|
4
|
+
import { env } from '@/lib/env'
|
|
5
|
+
|
|
6
|
+
const sql = neon(env.DATABASE_URL)
|
|
7
|
+
|
|
8
|
+
export const db = drizzle(sql, { schema })
|
|
9
|
+
|
|
10
|
+
export type Database = typeof db
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server-side environment variables schema
|
|
5
|
+
* These are validated at build time and runtime
|
|
6
|
+
*/
|
|
7
|
+
const serverEnvSchema = z.object({
|
|
8
|
+
// Database
|
|
9
|
+
DATABASE_URL: z.string().url().describe('Neon PostgreSQL connection string'),
|
|
10
|
+
|
|
11
|
+
// Authentication
|
|
12
|
+
AUTH_SECRET: z
|
|
13
|
+
.string()
|
|
14
|
+
.min(32)
|
|
15
|
+
.describe('NextAuth.js secret - generate with: openssl rand -base64 32'),
|
|
16
|
+
AUTH_GITHUB_ID: z.string().min(1).describe('GitHub OAuth App Client ID'),
|
|
17
|
+
AUTH_GITHUB_SECRET: z.string().min(1).describe('GitHub OAuth App Client Secret'),
|
|
18
|
+
|
|
19
|
+
// Optional: AI
|
|
20
|
+
OPENAI_API_KEY: z.string().optional().describe('OpenAI API key for AI features'),
|
|
21
|
+
|
|
22
|
+
// Node environment
|
|
23
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Client-side environment variables schema
|
|
28
|
+
* These are exposed to the browser (prefixed with NEXT_PUBLIC_)
|
|
29
|
+
*/
|
|
30
|
+
const clientEnvSchema = z.object({
|
|
31
|
+
NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate and export environment variables
|
|
36
|
+
*/
|
|
37
|
+
function validateEnv() {
|
|
38
|
+
// Skip validation during build if env vars aren't available
|
|
39
|
+
if (process.env.SKIP_ENV_VALIDATION === 'true') {
|
|
40
|
+
console.warn('⚠️ Skipping environment validation')
|
|
41
|
+
return {
|
|
42
|
+
DATABASE_URL: process.env.DATABASE_URL ?? '',
|
|
43
|
+
AUTH_SECRET: process.env.AUTH_SECRET ?? '',
|
|
44
|
+
AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID ?? '',
|
|
45
|
+
AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET ?? '',
|
|
46
|
+
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
|
47
|
+
NODE_ENV: (process.env.NODE_ENV as 'development' | 'production' | 'test') ?? 'development',
|
|
48
|
+
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const serverEnv = serverEnvSchema.safeParse(process.env)
|
|
53
|
+
const clientEnv = clientEnvSchema.safeParse(process.env)
|
|
54
|
+
|
|
55
|
+
if (!serverEnv.success) {
|
|
56
|
+
console.error('❌ Invalid server environment variables:')
|
|
57
|
+
console.error(serverEnv.error.flatten().fieldErrors)
|
|
58
|
+
throw new Error('Invalid server environment variables')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!clientEnv.success) {
|
|
62
|
+
console.error('❌ Invalid client environment variables:')
|
|
63
|
+
console.error(clientEnv.error.flatten().fieldErrors)
|
|
64
|
+
throw new Error('Invalid client environment variables')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
...serverEnv.data,
|
|
69
|
+
...clientEnv.data,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const env = validateEnv()
|
|
74
|
+
|
|
75
|
+
// Type for the validated environment
|
|
76
|
+
export type Env = typeof env
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NextConfig } from 'next'
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
// Enable React strict mode for better development experience
|
|
5
|
+
reactStrictMode: true,
|
|
6
|
+
|
|
7
|
+
// Experimental features
|
|
8
|
+
experimental: {
|
|
9
|
+
// Enable Partial Prerendering (PPR) for improved performance
|
|
10
|
+
// ppr: true,
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default nextConfig
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Static assets go here
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createCallerFactory, createTRPCRouter } from './trpc'
|
|
2
|
+
import { exampleRouter } from './routers/example'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This is the primary router for your server.
|
|
6
|
+
*
|
|
7
|
+
* All routers added in /api/routers should be manually added here.
|
|
8
|
+
*/
|
|
9
|
+
export const appRouter = createTRPCRouter({
|
|
10
|
+
example: exampleRouter,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// Export type definition of API
|
|
14
|
+
export type AppRouter = typeof appRouter
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a server-side caller for the tRPC API
|
|
18
|
+
* @example
|
|
19
|
+
* const trpc = createCaller(createContext)
|
|
20
|
+
* const result = await trpc.example.hello({ text: 'world' })
|
|
21
|
+
*/
|
|
22
|
+
export const createCaller = createCallerFactory(appRouter)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { createTRPCRouter, publicProcedure, protectedProcedure } from '../trpc'
|
|
3
|
+
import { posts } from '@/server/db/schema'
|
|
4
|
+
import { eq, desc } from 'drizzle-orm'
|
|
5
|
+
|
|
6
|
+
export const exampleRouter = createTRPCRouter({
|
|
7
|
+
/**
|
|
8
|
+
* Public procedure - accessible without authentication
|
|
9
|
+
*/
|
|
10
|
+
hello: publicProcedure
|
|
11
|
+
.input(z.object({ text: z.string().optional() }))
|
|
12
|
+
.query(({ input }) => {
|
|
13
|
+
return {
|
|
14
|
+
greeting: `Hello ${input.text ?? 'World'}!`,
|
|
15
|
+
}
|
|
16
|
+
}),
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get all posts - public
|
|
20
|
+
*/
|
|
21
|
+
getPosts: publicProcedure.query(async ({ ctx }) => {
|
|
22
|
+
const allPosts = await ctx.db.select().from(posts).orderBy(desc(posts.createdAt)).limit(10)
|
|
23
|
+
|
|
24
|
+
return allPosts
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get a single post by ID - public
|
|
29
|
+
*/
|
|
30
|
+
getPost: publicProcedure.input(z.object({ id: z.number() })).query(async ({ ctx, input }) => {
|
|
31
|
+
const post = await ctx.db.select().from(posts).where(eq(posts.id, input.id)).limit(1)
|
|
32
|
+
|
|
33
|
+
return post[0] ?? null
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a new post - protected (requires authentication)
|
|
38
|
+
*/
|
|
39
|
+
createPost: protectedProcedure
|
|
40
|
+
.input(
|
|
41
|
+
z.object({
|
|
42
|
+
title: z.string().min(1).max(256),
|
|
43
|
+
content: z.string().optional(),
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
.mutation(async ({ ctx, input }) => {
|
|
47
|
+
const newPost = await ctx.db
|
|
48
|
+
.insert(posts)
|
|
49
|
+
.values({
|
|
50
|
+
title: input.title,
|
|
51
|
+
content: input.content,
|
|
52
|
+
authorId: ctx.session.user.id,
|
|
53
|
+
})
|
|
54
|
+
.returning()
|
|
55
|
+
|
|
56
|
+
return newPost[0]
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Delete a post - protected
|
|
61
|
+
*/
|
|
62
|
+
deletePost: protectedProcedure
|
|
63
|
+
.input(z.object({ id: z.number() }))
|
|
64
|
+
.mutation(async ({ ctx, input }) => {
|
|
65
|
+
await ctx.db.delete(posts).where(eq(posts.id, input.id))
|
|
66
|
+
|
|
67
|
+
return { success: true }
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get secret message - protected (requires authentication)
|
|
72
|
+
*/
|
|
73
|
+
getSecretMessage: protectedProcedure.query(() => {
|
|
74
|
+
return 'You are authenticated! Here is the secret message.'
|
|
75
|
+
}),
|
|
76
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { initTRPC, TRPCError } from '@trpc/server'
|
|
2
|
+
import superjson from 'superjson'
|
|
3
|
+
import { ZodError } from 'zod'
|
|
4
|
+
import { db } from '@/lib/db'
|
|
5
|
+
import { auth } from '@/lib/auth'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Context creation for tRPC
|
|
9
|
+
* This is called for each incoming request
|
|
10
|
+
*/
|
|
11
|
+
export const createTRPCContext = async (opts: { headers: Headers }) => {
|
|
12
|
+
const session = await auth()
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
db,
|
|
16
|
+
session,
|
|
17
|
+
...opts,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize tRPC with context type
|
|
23
|
+
*/
|
|
24
|
+
const t = initTRPC.context<typeof createTRPCContext>().create({
|
|
25
|
+
transformer: superjson,
|
|
26
|
+
errorFormatter({ shape, error }) {
|
|
27
|
+
return {
|
|
28
|
+
...shape,
|
|
29
|
+
data: {
|
|
30
|
+
...shape.data,
|
|
31
|
+
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a server-side caller for tRPC
|
|
39
|
+
*/
|
|
40
|
+
export const createCallerFactory = t.createCallerFactory
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Router and procedure helpers
|
|
44
|
+
*/
|
|
45
|
+
export const createTRPCRouter = t.router
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Public (unauthenticated) procedure
|
|
49
|
+
*/
|
|
50
|
+
export const publicProcedure = t.procedure
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Protected (authenticated) procedure
|
|
54
|
+
* Throws UNAUTHORIZED error if user is not logged in
|
|
55
|
+
*/
|
|
56
|
+
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
|
57
|
+
if (!ctx.session || !ctx.session.user) {
|
|
58
|
+
throw new TRPCError({ code: 'UNAUTHORIZED' })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return next({
|
|
62
|
+
ctx: {
|
|
63
|
+
// Infers the `session` as non-nullable
|
|
64
|
+
session: { ...ctx.session, user: ctx.session.user },
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
})
|