nextforge-cli 1.0.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/README.md +187 -0
- package/bin/cli.js +666 -0
- package/package.json +45 -0
- package/templates/auth/ldap/ldap-service.ts +146 -0
- package/templates/base/app/dashboard/page.tsx +97 -0
- package/templates/base/app/globals.css +59 -0
- package/templates/base/app/layout.tsx +27 -0
- package/templates/base/app/login/page.tsx +118 -0
- package/templates/base/app/page.tsx +48 -0
- package/templates/base/app/register/page.tsx +164 -0
- package/templates/base/components/ui/button.tsx +52 -0
- package/templates/base/components/ui/card.tsx +78 -0
- package/templates/base/components/ui/input.tsx +24 -0
- package/templates/base/components/ui/label.tsx +23 -0
- package/templates/base/features/auth/server/route.ts +160 -0
- package/templates/base/index.ts +75 -0
- package/templates/base/lib/auth.ts +74 -0
- package/templates/base/lib/logger.ts +66 -0
- package/templates/base/lib/prisma.ts +15 -0
- package/templates/base/lib/rpc.ts +35 -0
- package/templates/base/lib/store.ts +53 -0
- package/templates/base/lib/utils.ts +6 -0
- package/templates/base/middleware/auth-middleware.ts +42 -0
- package/templates/base/next.config.js +10 -0
- package/templates/base/postcss.config.js +6 -0
- package/templates/base/providers/providers.tsx +32 -0
- package/templates/base/tailwind.config.ts +58 -0
- package/templates/base/tsconfig.json +26 -0
- package/templates/email/email-service.ts +120 -0
- package/templates/stripe/route.ts +147 -0
- package/templates/stripe/stripe-service.ts +117 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
12
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
13
|
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
14
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
15
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
16
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
default: "h-10 px-4 py-2",
|
|
20
|
+
sm: "h-9 rounded-md px-3",
|
|
21
|
+
lg: "h-11 rounded-md px-8",
|
|
22
|
+
icon: "h-10 w-10",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
variant: "default",
|
|
27
|
+
size: "default",
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export interface ButtonProps
|
|
33
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
34
|
+
VariantProps<typeof buttonVariants> {
|
|
35
|
+
asChild?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
39
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
40
|
+
const Comp = asChild ? Slot : "button"
|
|
41
|
+
return (
|
|
42
|
+
<Comp
|
|
43
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
44
|
+
ref={ref}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
Button.displayName = "Button"
|
|
51
|
+
|
|
52
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
const Card = React.forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<div
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
))
|
|
17
|
+
Card.displayName = "Card"
|
|
18
|
+
|
|
19
|
+
const CardHeader = React.forwardRef<
|
|
20
|
+
HTMLDivElement,
|
|
21
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
))
|
|
29
|
+
CardHeader.displayName = "CardHeader"
|
|
30
|
+
|
|
31
|
+
const CardTitle = React.forwardRef<
|
|
32
|
+
HTMLParagraphElement,
|
|
33
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
34
|
+
>(({ className, ...props }, ref) => (
|
|
35
|
+
<h3
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn(
|
|
38
|
+
"text-2xl font-semibold leading-none tracking-tight",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
))
|
|
44
|
+
CardTitle.displayName = "CardTitle"
|
|
45
|
+
|
|
46
|
+
const CardDescription = React.forwardRef<
|
|
47
|
+
HTMLParagraphElement,
|
|
48
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
49
|
+
>(({ className, ...props }, ref) => (
|
|
50
|
+
<p
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
))
|
|
56
|
+
CardDescription.displayName = "CardDescription"
|
|
57
|
+
|
|
58
|
+
const CardContent = React.forwardRef<
|
|
59
|
+
HTMLDivElement,
|
|
60
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
61
|
+
>(({ className, ...props }, ref) => (
|
|
62
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
63
|
+
))
|
|
64
|
+
CardContent.displayName = "CardContent"
|
|
65
|
+
|
|
66
|
+
const CardFooter = React.forwardRef<
|
|
67
|
+
HTMLDivElement,
|
|
68
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
69
|
+
>(({ className, ...props }, ref) => (
|
|
70
|
+
<div
|
|
71
|
+
ref={ref}
|
|
72
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
))
|
|
76
|
+
CardFooter.displayName = "CardFooter"
|
|
77
|
+
|
|
78
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
export interface InputProps
|
|
5
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
6
|
+
|
|
7
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
8
|
+
({ className, type, ...props }, ref) => {
|
|
9
|
+
return (
|
|
10
|
+
<input
|
|
11
|
+
type={type}
|
|
12
|
+
className={cn(
|
|
13
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
ref={ref}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
Input.displayName = "Input"
|
|
23
|
+
|
|
24
|
+
export { Input }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const labelVariants = cva(
|
|
7
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
const Label = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
13
|
+
VariantProps<typeof labelVariants>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<LabelPrimitive.Root
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(labelVariants(), className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
))
|
|
21
|
+
Label.displayName = LabelPrimitive.Root.displayName
|
|
22
|
+
|
|
23
|
+
export { Label }
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { zValidator } from '@hono/zod-validator';
|
|
3
|
+
import { setCookie, deleteCookie } from 'hono/cookie';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { prisma } from '../../../lib/prisma';
|
|
6
|
+
import { generateToken, comparePassword, hashPassword } from '../../../lib/auth';
|
|
7
|
+
import { logger } from '../../../lib/logger';
|
|
8
|
+
import { authMiddleware, requireRole } from '../../../middleware/auth-middleware';
|
|
9
|
+
|
|
10
|
+
const app = new Hono();
|
|
11
|
+
|
|
12
|
+
const loginSchema = z.object({
|
|
13
|
+
username: z.string().min(1),
|
|
14
|
+
password: z.string().min(1),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const registerSchema = z.object({
|
|
18
|
+
username: z.string().min(3),
|
|
19
|
+
email: z.string().email(),
|
|
20
|
+
password: z.string().min(8),
|
|
21
|
+
displayName: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Login
|
|
25
|
+
app.post('/login', zValidator('json', loginSchema), async (c) => {
|
|
26
|
+
const { username, password } = c.req.valid('json');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const user = await prisma.user.findUnique({
|
|
30
|
+
where: { username },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!user || !user.password) {
|
|
34
|
+
return c.json({ success: false, error: 'Invalid credentials' }, 401);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const validPassword = await comparePassword(password, user.password);
|
|
38
|
+
if (!validPassword) {
|
|
39
|
+
return c.json({ success: false, error: 'Invalid credentials' }, 401);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const token = generateToken(user);
|
|
43
|
+
|
|
44
|
+
setCookie(c, 'auth_token', token, {
|
|
45
|
+
httpOnly: true,
|
|
46
|
+
secure: process.env.NODE_ENV === 'production',
|
|
47
|
+
sameSite: 'Lax',
|
|
48
|
+
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
49
|
+
path: '/',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
logger.info('Login successful', { username: user.username, role: user.role });
|
|
53
|
+
|
|
54
|
+
return c.json({
|
|
55
|
+
success: true,
|
|
56
|
+
data: {
|
|
57
|
+
user: {
|
|
58
|
+
id: user.id,
|
|
59
|
+
username: user.username,
|
|
60
|
+
email: user.email,
|
|
61
|
+
displayName: user.displayName,
|
|
62
|
+
role: user.role,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
logger.error('Login error', { error: error.message });
|
|
68
|
+
return c.json({ success: false, error: 'Login failed' }, 500);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Register
|
|
73
|
+
app.post('/register', zValidator('json', registerSchema), async (c) => {
|
|
74
|
+
const { username, email, password, displayName } = c.req.valid('json');
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Check if user exists
|
|
78
|
+
const existing = await prisma.user.findFirst({
|
|
79
|
+
where: { OR: [{ username }, { email }] },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (existing) {
|
|
83
|
+
return c.json({ success: false, error: 'User already exists' }, 400);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const hashedPassword = await hashPassword(password);
|
|
87
|
+
|
|
88
|
+
const user = await prisma.user.create({
|
|
89
|
+
data: {
|
|
90
|
+
username,
|
|
91
|
+
email,
|
|
92
|
+
password: hashedPassword,
|
|
93
|
+
displayName: displayName || username,
|
|
94
|
+
role: 'user',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const token = generateToken(user);
|
|
99
|
+
|
|
100
|
+
setCookie(c, 'auth_token', token, {
|
|
101
|
+
httpOnly: true,
|
|
102
|
+
secure: process.env.NODE_ENV === 'production',
|
|
103
|
+
sameSite: 'Lax',
|
|
104
|
+
maxAge: 60 * 60 * 24 * 7,
|
|
105
|
+
path: '/',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
logger.info('User registered', { username: user.username });
|
|
109
|
+
|
|
110
|
+
return c.json({
|
|
111
|
+
success: true,
|
|
112
|
+
data: {
|
|
113
|
+
user: {
|
|
114
|
+
id: user.id,
|
|
115
|
+
username: user.username,
|
|
116
|
+
email: user.email,
|
|
117
|
+
displayName: user.displayName,
|
|
118
|
+
role: user.role,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
} catch (error: any) {
|
|
123
|
+
logger.error('Registration error', { error: error.message });
|
|
124
|
+
return c.json({ success: false, error: 'Registration failed' }, 500);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Logout
|
|
129
|
+
app.post('/logout', authMiddleware, (c) => {
|
|
130
|
+
deleteCookie(c, 'auth_token');
|
|
131
|
+
return c.json({ success: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Get current user
|
|
135
|
+
app.get('/me', authMiddleware, (c) => {
|
|
136
|
+
const user = c.get('user');
|
|
137
|
+
return c.json({ success: true, data: user });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Admin: Get all users
|
|
141
|
+
app.get('/users', authMiddleware, requireRole('admin'), async (c) => {
|
|
142
|
+
try {
|
|
143
|
+
const users = await prisma.user.findMany({
|
|
144
|
+
select: {
|
|
145
|
+
id: true,
|
|
146
|
+
username: true,
|
|
147
|
+
email: true,
|
|
148
|
+
displayName: true,
|
|
149
|
+
role: true,
|
|
150
|
+
createdAt: true,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
return c.json({ success: true, data: users });
|
|
154
|
+
} catch (error: any) {
|
|
155
|
+
logger.error('Failed to get users', { error: error.message });
|
|
156
|
+
return c.json({ success: false, error: 'Failed to get users' }, 500);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export default app;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { serve } from '@hono/node-server';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { cors } from 'hono/cors';
|
|
5
|
+
import { logger as honoLogger } from 'hono/logger';
|
|
6
|
+
import { prettyJSON } from 'hono/pretty-json';
|
|
7
|
+
import { logger } from './lib/logger';
|
|
8
|
+
|
|
9
|
+
// Import routes
|
|
10
|
+
import authRoute from './features/auth/server/route';
|
|
11
|
+
// Add more routes as needed
|
|
12
|
+
|
|
13
|
+
const app = new Hono();
|
|
14
|
+
|
|
15
|
+
// Middleware
|
|
16
|
+
app.use('*', cors({
|
|
17
|
+
origin: (origin) => {
|
|
18
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
19
|
+
return origin || '*';
|
|
20
|
+
}
|
|
21
|
+
const appUrl = process.env.APP_URL || 'http://localhost:3001';
|
|
22
|
+
const allowedOrigins = [
|
|
23
|
+
'http://localhost:3001',
|
|
24
|
+
appUrl,
|
|
25
|
+
];
|
|
26
|
+
return allowedOrigins.includes(origin || '') ? origin : appUrl;
|
|
27
|
+
},
|
|
28
|
+
credentials: true,
|
|
29
|
+
}));
|
|
30
|
+
app.use('*', prettyJSON());
|
|
31
|
+
app.use('*', honoLogger());
|
|
32
|
+
|
|
33
|
+
// Custom logging middleware
|
|
34
|
+
app.use('*', async (c, next) => {
|
|
35
|
+
const start = Date.now();
|
|
36
|
+
await next();
|
|
37
|
+
const duration = Date.now() - start;
|
|
38
|
+
logger.info(`${c.req.method} ${c.req.path}`, {
|
|
39
|
+
method: c.req.method,
|
|
40
|
+
path: c.req.path,
|
|
41
|
+
status: c.res.status,
|
|
42
|
+
duration: `${duration}ms`,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Health check
|
|
47
|
+
app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }));
|
|
48
|
+
|
|
49
|
+
// Routes
|
|
50
|
+
app.route('/api/auth', authRoute);
|
|
51
|
+
|
|
52
|
+
// Root endpoint
|
|
53
|
+
app.get('/', (c) => {
|
|
54
|
+
return c.json({
|
|
55
|
+
name: process.env.APP_NAME || 'My App',
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
status: 'running',
|
|
58
|
+
endpoints: {
|
|
59
|
+
health: '/health',
|
|
60
|
+
auth: '/api/auth',
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Start server
|
|
66
|
+
const port = parseInt(process.env.API_PORT || '3000');
|
|
67
|
+
console.log(`🚀 API Server running on http://localhost:${port}`);
|
|
68
|
+
|
|
69
|
+
serve({
|
|
70
|
+
fetch: app.fetch,
|
|
71
|
+
port,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export default app;
|
|
75
|
+
export type AppType = typeof app;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import bcrypt from 'bcrypt';
|
|
3
|
+
import { logger } from './logger';
|
|
4
|
+
|
|
5
|
+
export type UserRole = 'user' | 'admin' | 'super_admin';
|
|
6
|
+
|
|
7
|
+
export interface User {
|
|
8
|
+
id: number;
|
|
9
|
+
username: string;
|
|
10
|
+
email?: string | null;
|
|
11
|
+
displayName?: string | null;
|
|
12
|
+
role: UserRole;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface JWTPayload {
|
|
16
|
+
userId: number;
|
|
17
|
+
username: string;
|
|
18
|
+
email?: string | null;
|
|
19
|
+
displayName?: string | null;
|
|
20
|
+
role: UserRole;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
24
|
+
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
|
25
|
+
|
|
26
|
+
export function generateToken(user: User): string {
|
|
27
|
+
const payload: JWTPayload = {
|
|
28
|
+
userId: user.id,
|
|
29
|
+
username: user.username,
|
|
30
|
+
email: user.email,
|
|
31
|
+
displayName: user.displayName,
|
|
32
|
+
role: user.role as UserRole,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function verifyToken(token: string): JWTPayload | null {
|
|
39
|
+
try {
|
|
40
|
+
return jwt.verify(token, JWT_SECRET) as JWTPayload;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger.warn('Token verification failed', { error });
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
48
|
+
return bcrypt.hash(password, 10);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function comparePassword(password: string, hash: string): Promise<boolean> {
|
|
52
|
+
return bcrypt.compare(password, hash);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Role hierarchy
|
|
56
|
+
const ROLE_HIERARCHY: Record<UserRole, number> = {
|
|
57
|
+
user: 1,
|
|
58
|
+
admin: 2,
|
|
59
|
+
super_admin: 3,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function hasPermission(user: User | JWTPayload, requiredRole: UserRole): boolean {
|
|
63
|
+
const userLevel = ROLE_HIERARCHY[user.role];
|
|
64
|
+
const requiredLevel = ROLE_HIERARCHY[requiredRole];
|
|
65
|
+
return userLevel >= requiredLevel;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isAdmin(user: User | JWTPayload): boolean {
|
|
69
|
+
return hasPermission(user, 'admin');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isSuperAdmin(user: User | JWTPayload): boolean {
|
|
73
|
+
return user.role === 'super_admin';
|
|
74
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import DailyRotateFile from 'winston-daily-rotate-file';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const LOG_DIR = process.env.LOG_DIR || './logs';
|
|
6
|
+
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
|
|
7
|
+
|
|
8
|
+
const customFormat = winston.format.combine(
|
|
9
|
+
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
10
|
+
winston.format.errors({ stack: true }),
|
|
11
|
+
winston.format.printf(({ level, message, timestamp, ...meta }) => {
|
|
12
|
+
const metaStr = Object.keys(meta).length ? `\n${JSON.stringify(meta, null, 2)}` : '';
|
|
13
|
+
return `${timestamp} [${level.toUpperCase()}]: ${message}${metaStr}`;
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const consoleFormat = winston.format.combine(
|
|
18
|
+
winston.format.colorize(),
|
|
19
|
+
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
20
|
+
winston.format.printf(({ level, message, timestamp }) => {
|
|
21
|
+
return `${timestamp} [${level}]: ${message}`;
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Create transports
|
|
26
|
+
const transports: winston.transport[] = [
|
|
27
|
+
new winston.transports.Console({
|
|
28
|
+
format: consoleFormat,
|
|
29
|
+
}),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Add file transports in production
|
|
33
|
+
if (process.env.NODE_ENV === 'production') {
|
|
34
|
+
transports.push(
|
|
35
|
+
new DailyRotateFile({
|
|
36
|
+
dirname: LOG_DIR,
|
|
37
|
+
filename: 'app-%DATE%.log',
|
|
38
|
+
datePattern: 'YYYY-MM-DD',
|
|
39
|
+
maxSize: '20m',
|
|
40
|
+
maxFiles: '14d',
|
|
41
|
+
format: customFormat,
|
|
42
|
+
}),
|
|
43
|
+
new DailyRotateFile({
|
|
44
|
+
dirname: LOG_DIR,
|
|
45
|
+
filename: 'error-%DATE%.log',
|
|
46
|
+
datePattern: 'YYYY-MM-DD',
|
|
47
|
+
maxSize: '20m',
|
|
48
|
+
maxFiles: '30d',
|
|
49
|
+
level: 'error',
|
|
50
|
+
format: customFormat,
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const logger = winston.createLogger({
|
|
56
|
+
level: LOG_LEVEL,
|
|
57
|
+
format: customFormat,
|
|
58
|
+
transports,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Create child loggers for specific modules
|
|
62
|
+
export const createLogger = (module: string) => {
|
|
63
|
+
return logger.child({ module });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default logger;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client';
|
|
2
|
+
|
|
3
|
+
const globalForPrisma = globalThis as unknown as {
|
|
4
|
+
prisma: PrismaClient | undefined;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const prisma =
|
|
8
|
+
globalForPrisma.prisma ??
|
|
9
|
+
new PrismaClient({
|
|
10
|
+
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
|
14
|
+
|
|
15
|
+
export default prisma;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AppType } from '../index';
|
|
2
|
+
import { hc } from 'hono/client';
|
|
3
|
+
|
|
4
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
|
5
|
+
|
|
6
|
+
export const client = hc<AppType>(API_URL, {
|
|
7
|
+
fetch: (input, init) =>
|
|
8
|
+
fetch(input, {
|
|
9
|
+
...init,
|
|
10
|
+
credentials: 'include',
|
|
11
|
+
}),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Helper function for RPC calls with error handling
|
|
15
|
+
export async function rpc<T>(
|
|
16
|
+
fn: () => Promise<{ json: () => Promise<{ success: boolean; data?: T; error?: string }> }>
|
|
17
|
+
): Promise<T> {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fn();
|
|
20
|
+
const data = await res.json();
|
|
21
|
+
|
|
22
|
+
if (!data.success) {
|
|
23
|
+
throw new Error(data.error || 'Unknown error');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return data.data as T;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof Error) {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
throw new Error('Network error');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default client;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
interface User {
|
|
5
|
+
id: number;
|
|
6
|
+
username: string;
|
|
7
|
+
email?: string | null;
|
|
8
|
+
displayName?: string | null;
|
|
9
|
+
role: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AuthState {
|
|
13
|
+
user: User | null;
|
|
14
|
+
isAuthenticated: boolean;
|
|
15
|
+
setUser: (user: User | null) => void;
|
|
16
|
+
logout: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const useAuthStore = create<AuthState>()(
|
|
20
|
+
persist(
|
|
21
|
+
(set) => ({
|
|
22
|
+
user: null,
|
|
23
|
+
isAuthenticated: false,
|
|
24
|
+
setUser: (user) => set({ user, isAuthenticated: !!user }),
|
|
25
|
+
logout: () => set({ user: null, isAuthenticated: false }),
|
|
26
|
+
}),
|
|
27
|
+
{
|
|
28
|
+
name: 'auth-storage',
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Generic store creator for app-specific state
|
|
34
|
+
interface AppState {
|
|
35
|
+
sidebarOpen: boolean;
|
|
36
|
+
theme: 'light' | 'dark' | 'system';
|
|
37
|
+
toggleSidebar: () => void;
|
|
38
|
+
setTheme: (theme: 'light' | 'dark' | 'system') => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const useAppStore = create<AppState>()(
|
|
42
|
+
persist(
|
|
43
|
+
(set) => ({
|
|
44
|
+
sidebarOpen: true,
|
|
45
|
+
theme: 'system',
|
|
46
|
+
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
|
47
|
+
setTheme: (theme) => set({ theme }),
|
|
48
|
+
}),
|
|
49
|
+
{
|
|
50
|
+
name: 'app-storage',
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
);
|