abdellah0l-stack 1.0.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 +50 -0
- package/bin/cli.js +218 -0
- package/package.json +30 -0
- package/template/README.md +61 -0
- package/template/components.json +22 -0
- package/template/drizzle.config.ts +13 -0
- package/template/eslint.config.mjs +25 -0
- package/template/next.config.ts +114 -0
- package/template/package.json +62 -0
- package/template/postcss.config.mjs +7 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/next.svg +1 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/src/app/api/v1/auth/[...all]/route.ts +5 -0
- package/template/src/app/api/v1/trpc/[trpc]/route.ts +13 -0
- package/template/src/app/api/v1/uploadthing/core.ts +50 -0
- package/template/src/app/api/v1/uploadthing/route.ts +11 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +121 -0
- package/template/src/app/layout.tsx +50 -0
- package/template/src/app/page.tsx +58 -0
- package/template/src/components/loading-spinner.tsx +18 -0
- package/template/src/components/navigation.tsx +54 -0
- package/template/src/components/query-provider.tsx +27 -0
- package/template/src/components/ui/badge.tsx +46 -0
- package/template/src/components/ui/button.tsx +58 -0
- package/template/src/components/ui/card.tsx +92 -0
- package/template/src/components/ui/dialog.tsx +143 -0
- package/template/src/components/ui/input.tsx +21 -0
- package/template/src/components/ui/label.tsx +26 -0
- package/template/src/components/ui/select.tsx +185 -0
- package/template/src/components/ui/tabs.tsx +55 -0
- package/template/src/components/ui/textarea.tsx +23 -0
- package/template/src/data/env/client.ts +7 -0
- package/template/src/data/env/server.ts +13 -0
- package/template/src/drizzle/db.ts +6 -0
- package/template/src/drizzle/schema/app-schema.ts +31 -0
- package/template/src/drizzle/schema/auth-schema.ts +55 -0
- package/template/src/drizzle/schema/index.ts +14 -0
- package/template/src/hooks/use-auth.ts +32 -0
- package/template/src/hooks/use-debounce.ts +18 -0
- package/template/src/lib/arcjet.ts +45 -0
- package/template/src/lib/auth-client.ts +7 -0
- package/template/src/lib/auth.ts +50 -0
- package/template/src/lib/use-mobile.ts +29 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/middleware.ts +14 -0
- package/template/src/server/index.ts +13 -0
- package/template/src/server/routers/posts.ts +93 -0
- package/template/src/server/routers/users.ts +56 -0
- package/template/src/server/trpc.ts +38 -0
- package/template/src/types/index.ts +10 -0
- package/template/src/utils/trpc.ts +5 -0
- package/template/src/utils/uploadthing.ts +10 -0
- package/template/tsconfig.json +42 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
|
+
import { db } from "@/drizzle/db";
|
|
4
|
+
import { nextCookies } from "better-auth/next-js";
|
|
5
|
+
import { headers } from "next/headers";
|
|
6
|
+
|
|
7
|
+
// authentication instance to be used in API routes and server-side functions
|
|
8
|
+
export const auth = betterAuth({
|
|
9
|
+
basePath: "/api/v1/auth",
|
|
10
|
+
database: drizzleAdapter(db, {
|
|
11
|
+
provider: "pg",
|
|
12
|
+
}),
|
|
13
|
+
emailAndPassword: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
},
|
|
16
|
+
socialProviders: {
|
|
17
|
+
github: {
|
|
18
|
+
clientId: process.env.GITHUB_CLIENT_ID as string || "",
|
|
19
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET as string || "",
|
|
20
|
+
},
|
|
21
|
+
google: {
|
|
22
|
+
clientId: process.env.GOOGLE_CLIENT_ID as string || "",
|
|
23
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string || "",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
session: {
|
|
27
|
+
cookieCache: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
maxAge: 60 * 5, // 5 minutes
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
plugins: [nextCookies()],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// get full session data
|
|
36
|
+
export async function getSession() {
|
|
37
|
+
try {
|
|
38
|
+
const headersList = await headers();
|
|
39
|
+
|
|
40
|
+
// Try to get session using the built-in better-auth session handling
|
|
41
|
+
const session = await auth.api.getSession({
|
|
42
|
+
headers: headersList,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return session;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error getting session:', error);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
// a custom hook to determine if the current device is mobile based on window width
|
|
6
|
+
export function useMobile() {
|
|
7
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
|
|
11
|
+
const checkIsMobile = () => {
|
|
12
|
+
setIsMobile(window.innerWidth < 768);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
checkIsMobile();
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
window.addEventListener("resize", checkIsMobile);
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
return () => {
|
|
23
|
+
window.removeEventListener("resize", checkIsMobile);
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return isMobile;
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from "next/server"
|
|
2
|
+
|
|
3
|
+
// middleware function that runs on every request matching the specified matcher
|
|
4
|
+
|
|
5
|
+
export async function middleware(request: NextRequest) {
|
|
6
|
+
return NextResponse.next();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const config = {
|
|
10
|
+
matcher: [
|
|
11
|
+
// match all paths except for static files and API routes
|
|
12
|
+
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
|
13
|
+
],
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { router } from "./trpc";
|
|
2
|
+
import { postsRouter } from "./routers/posts";
|
|
3
|
+
import { usersRouter } from "./routers/users";
|
|
4
|
+
|
|
5
|
+
// main application router combining all individual routers
|
|
6
|
+
export const appRouter = router({
|
|
7
|
+
// example routers
|
|
8
|
+
posts: postsRouter,
|
|
9
|
+
users: usersRouter,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// export the type definition of the app router
|
|
13
|
+
export type AppRouter = typeof appRouter;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { router, protectedProcedure, publicProcedure } from "../trpc";
|
|
3
|
+
import { db } from "@/drizzle/db";
|
|
4
|
+
import { posts } from "@/drizzle/schema";
|
|
5
|
+
import { eq, desc } from "drizzle-orm";
|
|
6
|
+
|
|
7
|
+
// this is an example router file for managing blog posts
|
|
8
|
+
|
|
9
|
+
export const postsRouter = router({
|
|
10
|
+
// Get all posts
|
|
11
|
+
list: publicProcedure.query(async () => {
|
|
12
|
+
return await db.select().from(posts).orderBy(desc(posts.createdAt));
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
// Get single post by ID
|
|
16
|
+
byId: publicProcedure
|
|
17
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
18
|
+
.query(async ({ input }) => {
|
|
19
|
+
const result = await db
|
|
20
|
+
.select()
|
|
21
|
+
.from(posts)
|
|
22
|
+
.where(eq(posts.id, input.id));
|
|
23
|
+
return result[0] ?? null;
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
// Create a new post (protected)
|
|
27
|
+
create: protectedProcedure
|
|
28
|
+
.input(
|
|
29
|
+
z.object({
|
|
30
|
+
title: z.string().min(1).max(255),
|
|
31
|
+
content: z.string().min(1),
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
.mutation(async ({ ctx, input }) => {
|
|
35
|
+
const [post] = await db
|
|
36
|
+
.insert(posts)
|
|
37
|
+
.values({
|
|
38
|
+
title: input.title,
|
|
39
|
+
content: input.content,
|
|
40
|
+
userId: ctx.user.id,
|
|
41
|
+
})
|
|
42
|
+
.returning();
|
|
43
|
+
return post;
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
// Update a post (protected, owner only)
|
|
47
|
+
update: protectedProcedure
|
|
48
|
+
.input(
|
|
49
|
+
z.object({
|
|
50
|
+
id: z.string().uuid(),
|
|
51
|
+
title: z.string().min(1).max(255).optional(),
|
|
52
|
+
content: z.string().min(1).optional(),
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
.mutation(async ({ ctx, input }) => {
|
|
56
|
+
const [existing] = await db
|
|
57
|
+
.select()
|
|
58
|
+
.from(posts)
|
|
59
|
+
.where(eq(posts.id, input.id));
|
|
60
|
+
|
|
61
|
+
if (!existing || existing.userId !== ctx.user.id) {
|
|
62
|
+
throw new Error("Not authorized");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const [updated] = await db
|
|
66
|
+
.update(posts)
|
|
67
|
+
.set({
|
|
68
|
+
...(input.title && { title: input.title }),
|
|
69
|
+
...(input.content && { content: input.content }),
|
|
70
|
+
})
|
|
71
|
+
.where(eq(posts.id, input.id))
|
|
72
|
+
.returning();
|
|
73
|
+
|
|
74
|
+
return updated;
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
// Delete a post (protected, owner only)
|
|
78
|
+
delete: protectedProcedure
|
|
79
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
80
|
+
.mutation(async ({ ctx, input }) => {
|
|
81
|
+
const [existing] = await db
|
|
82
|
+
.select()
|
|
83
|
+
.from(posts)
|
|
84
|
+
.where(eq(posts.id, input.id));
|
|
85
|
+
|
|
86
|
+
if (!existing || existing.userId !== ctx.user.id) {
|
|
87
|
+
throw new Error("Not authorized");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await db.delete(posts).where(eq(posts.id, input.id));
|
|
91
|
+
return { success: true };
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { router, publicProcedure, protectedProcedure } from "../trpc";
|
|
3
|
+
import { db } from "@/drizzle/db";
|
|
4
|
+
import { user } from "@/drizzle/schema/auth-schema";
|
|
5
|
+
import { eq } from "drizzle-orm";
|
|
6
|
+
|
|
7
|
+
// this is an example router file for managing users
|
|
8
|
+
// the ctx in protectedProcedure contains the authenticated user's session info
|
|
9
|
+
// and the input is actually a parameter passed to the procedure from the client-side
|
|
10
|
+
|
|
11
|
+
export const usersRouter = router({
|
|
12
|
+
// Get current user profile
|
|
13
|
+
me: protectedProcedure.query(async ({ ctx }) => {
|
|
14
|
+
const [profile] = await db
|
|
15
|
+
.select()
|
|
16
|
+
.from(user)
|
|
17
|
+
.where(eq(user.id, ctx.user.id));
|
|
18
|
+
return profile ?? null;
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
// Get user by ID (public)
|
|
22
|
+
byId: publicProcedure
|
|
23
|
+
.input(z.object({ id: z.string() }))
|
|
24
|
+
.query(async ({ input }) => {
|
|
25
|
+
const [profile] = await db
|
|
26
|
+
.select({
|
|
27
|
+
id: user.id,
|
|
28
|
+
name: user.name,
|
|
29
|
+
image: user.image,
|
|
30
|
+
createdAt: user.createdAt,
|
|
31
|
+
})
|
|
32
|
+
.from(user)
|
|
33
|
+
.where(eq(user.id, input.id));
|
|
34
|
+
return profile ?? null;
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
// Update current user profile
|
|
38
|
+
update: protectedProcedure
|
|
39
|
+
.input(
|
|
40
|
+
z.object({
|
|
41
|
+
name: z.string().min(1).max(100).optional(),
|
|
42
|
+
image: z.string().url().optional(),
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
.mutation(async ({ ctx, input }) => {
|
|
46
|
+
const [updated] = await db
|
|
47
|
+
.update(user)
|
|
48
|
+
.set({
|
|
49
|
+
...(input.name && { name: input.name }),
|
|
50
|
+
...(input.image && { image: input.image }),
|
|
51
|
+
})
|
|
52
|
+
.where(eq(user.id, ctx.user.id))
|
|
53
|
+
.returning();
|
|
54
|
+
return updated;
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { initTRPC, TRPCError } from '@trpc/server';
|
|
2
|
+
import { getSession } from '@/lib/auth';
|
|
3
|
+
import { aj } from '@/lib/arcjet';
|
|
4
|
+
|
|
5
|
+
// create the tRPC context which includes session data and request info
|
|
6
|
+
export const createTRPCContext = async (opts: { req: Request }) => {
|
|
7
|
+
const session = await getSession();
|
|
8
|
+
return {
|
|
9
|
+
session,
|
|
10
|
+
req: opts.req,
|
|
11
|
+
arcjet: aj,
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// initialize tRPC with the created context
|
|
16
|
+
const t = initTRPC.context<typeof createTRPCContext>().create();
|
|
17
|
+
|
|
18
|
+
// create the main router and procedures for tRPC
|
|
19
|
+
export const router = t.router;
|
|
20
|
+
|
|
21
|
+
// public procedure that does not require authentication
|
|
22
|
+
export const publicProcedure = t.procedure;
|
|
23
|
+
|
|
24
|
+
// protected procedure that requires the user to be logged in
|
|
25
|
+
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
|
|
26
|
+
if (!ctx.session || !ctx.session.user) {
|
|
27
|
+
throw new TRPCError({
|
|
28
|
+
code: 'UNAUTHORIZED',
|
|
29
|
+
message: 'You must be logged in to access this feature.'
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return next({
|
|
34
|
+
ctx: {
|
|
35
|
+
session: { ...ctx.session, user: ctx.session.user },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createTRPCReact } from '@trpc/react-query';
|
|
2
|
+
import type { AppRouter } from '@/server';
|
|
3
|
+
|
|
4
|
+
// create a tRPC React hook for the AppRouter, the job of this file is to provide type-safe hooks for tRPC in React components
|
|
5
|
+
export const trpc = createTRPCReact<AppRouter>();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateUploadButton,
|
|
3
|
+
} from "@uploadthing/react";
|
|
4
|
+
|
|
5
|
+
import type { OurFileRouter } from "../app/api/v1/uploadthing/core";
|
|
6
|
+
|
|
7
|
+
// UploadButton component for uploading files using Uploadthing
|
|
8
|
+
export const UploadButton = generateUploadButton<OurFileRouter>({
|
|
9
|
+
url: "/api/v1/uploadthing",
|
|
10
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"esnext"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"module": "esnext",
|
|
15
|
+
"moduleResolution": "bundler",
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
"incremental": true,
|
|
20
|
+
"plugins": [
|
|
21
|
+
{
|
|
22
|
+
"name": "next"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"paths": {
|
|
26
|
+
"@/*": [
|
|
27
|
+
"./src/*"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"include": [
|
|
32
|
+
"next-env.d.ts",
|
|
33
|
+
"**/*.ts",
|
|
34
|
+
"**/*.tsx",
|
|
35
|
+
".next/types/**/*.ts",
|
|
36
|
+
"**/*.mts",
|
|
37
|
+
".next/dev/types/**/*.ts"
|
|
38
|
+
],
|
|
39
|
+
"exclude": [
|
|
40
|
+
"node_modules"
|
|
41
|
+
]
|
|
42
|
+
}
|