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.
@@ -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
+ );
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }