@erwininteractive/mvc 0.6.5 → 0.7.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/dist/cli.js +7 -5
- package/dist/generators/initApp.d.ts +1 -0
- package/dist/generators/initApp.js +52 -0
- package/package.json +1 -1
- package/templates/appScaffold/postcss.config.cjs +9 -0
- package/templates/appScaffold/src/assets/tailwind.css +3 -0
- package/templates/appScaffold/tailwind.config.cjs +23 -0
- package/templates/auth/authController.ts.ejs +117 -0
package/dist/cli.js
CHANGED
|
@@ -16,11 +16,12 @@ program
|
|
|
16
16
|
.version("0.2.0")
|
|
17
17
|
.addHelpText("after", `
|
|
18
18
|
Examples:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
$ erwinmvc init myapp Create a new app
|
|
20
|
+
$ erwinmvc init myapp --with-tailwind Create app with Tailwind CSS
|
|
21
|
+
$ erwinmvc generate resource Post Generate CRUD for Post
|
|
22
|
+
$ erwinmvc webauthn Setup passkey authentication
|
|
23
|
+
$ erwinmvc make:auth Generate authentication system
|
|
24
|
+
$ erwinmvc list:routes Show all routes
|
|
24
25
|
`);
|
|
25
26
|
// Init command - scaffold a new application
|
|
26
27
|
program
|
|
@@ -29,6 +30,7 @@ program
|
|
|
29
30
|
.option("--skip-install", "Skip npm install")
|
|
30
31
|
.option("--with-database", "Include database/Prisma setup")
|
|
31
32
|
.option("--with-ci", "Include GitHub Actions CI workflow")
|
|
33
|
+
.option("--with-tailwind", "Include Tailwind CSS setup (with PostCSS)")
|
|
32
34
|
.action(async (dir, options) => {
|
|
33
35
|
try {
|
|
34
36
|
await (0, initApp_1.initApp)(dir, options);
|
|
@@ -72,6 +72,10 @@ async function initApp(dir, options = {}) {
|
|
|
72
72
|
if (options.withCi) {
|
|
73
73
|
setupCi(targetDir);
|
|
74
74
|
}
|
|
75
|
+
// Setup Tailwind CSS if requested
|
|
76
|
+
if (options.withTailwind) {
|
|
77
|
+
setupTailwind(targetDir);
|
|
78
|
+
}
|
|
75
79
|
console.log(`
|
|
76
80
|
Next steps:
|
|
77
81
|
cd ${dir}
|
|
@@ -83,6 +87,11 @@ To add database support later:
|
|
|
83
87
|
npm run db:setup
|
|
84
88
|
# Edit .env with DATABASE_URL
|
|
85
89
|
npx prisma migrate dev --name init
|
|
90
|
+
` : ""}
|
|
91
|
+
${options.withTailwind ? `
|
|
92
|
+
To configure Tailwind CSS:
|
|
93
|
+
npm run tailwind
|
|
94
|
+
# Edit tailwind.config.cjs and src/assets/tailwind.css
|
|
86
95
|
` : ""}`);
|
|
87
96
|
}
|
|
88
97
|
/**
|
|
@@ -121,6 +130,49 @@ function setupDatabase(targetDir) {
|
|
|
121
130
|
console.error("Failed to setup Prisma. Run 'npm run db:setup' manually.");
|
|
122
131
|
}
|
|
123
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Setup Tailwind CSS with PostCSS.
|
|
135
|
+
*/
|
|
136
|
+
function setupTailwind(targetDir) {
|
|
137
|
+
console.log("\nSetting up Tailwind CSS...");
|
|
138
|
+
try {
|
|
139
|
+
(0, child_process_1.execSync)("npm install -D tailwindcss postcss autoprefixer", { cwd: targetDir, stdio: "inherit" });
|
|
140
|
+
// Removed npx command - using manual config
|
|
141
|
+
// Create assets directory
|
|
142
|
+
const assetsDir = path_1.default.join(targetDir, "src", "assets");
|
|
143
|
+
fs_1.default.mkdirSync(assetsDir, { recursive: true });
|
|
144
|
+
// Create tailwind.css
|
|
145
|
+
const tailwindCss = `@tailwind base;
|
|
146
|
+
@tailwind components;
|
|
147
|
+
@tailwind utilities;
|
|
148
|
+
`;
|
|
149
|
+
fs_1.default.writeFileSync(path_1.default.join(assetsDir, "tailwind.css"), tailwindCss);
|
|
150
|
+
// Update postcss.config.cjs
|
|
151
|
+
const postcssConfig = `/** @type {import('postcss-load-config').Config} */
|
|
152
|
+
const config = {
|
|
153
|
+
plugins: {
|
|
154
|
+
tailwindcss: {},
|
|
155
|
+
autoprefixer: {},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export default config;
|
|
160
|
+
`;
|
|
161
|
+
fs_1.default.writeFileSync(path_1.default.join(targetDir, "postcss.config.cjs"), postcssConfig);
|
|
162
|
+
// Update package.json with tailwind build script
|
|
163
|
+
const packageJsonPath = path_1.default.join(targetDir, "package.json");
|
|
164
|
+
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
165
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf-8"));
|
|
166
|
+
pkg.scripts.tailwind = "tailwindcss -i ./src/assets/tailwind.css -o ./public/dist/tailwind.css --watch";
|
|
167
|
+
pkg.scripts.build = "tsc && npm run tailwind";
|
|
168
|
+
fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
|
|
169
|
+
console.log("✓ Added Tailwind CSS support");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
console.error("Failed to setup Tailwind CSS. Run 'npm install -D tailwindcss postcss autoprefixer' manually.");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
124
176
|
/**
|
|
125
177
|
* Recursively copy a directory.
|
|
126
178
|
*/
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
content: ["./src/**/*.{js,ts,tsx}"],
|
|
4
|
+
theme: {
|
|
5
|
+
extend: {
|
|
6
|
+
colors: {
|
|
7
|
+
primary: {
|
|
8
|
+
50: '#f0f9ff',
|
|
9
|
+
100: '#e0f2fe',
|
|
10
|
+
200: '#bae6fd',
|
|
11
|
+
300: '#7dd3fc',
|
|
12
|
+
400: '#38bdf8',
|
|
13
|
+
500: '#0ea5e9',
|
|
14
|
+
600: '#0284c7',
|
|
15
|
+
700: '#0369a1',
|
|
16
|
+
800: '#075985',
|
|
17
|
+
900: '#0c4a6e',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
plugins: [],
|
|
23
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import { hashPassword, verifyPassword, signToken, verifyToken } from "@erwininteractive/mvc";
|
|
3
|
+
import { getPrismaClient } from "@erwininteractive/mvc";
|
|
4
|
+
|
|
5
|
+
const prisma = getPrismaClient();
|
|
6
|
+
|
|
7
|
+
export async function showLogin(req: Request, res: Response) {
|
|
8
|
+
res.render("auth/login", { title: "Login" });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function showRegister(req: Request, res: Response) {
|
|
12
|
+
res.render("auth/register", { title: "Register" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function login(req: Request, res: Response) {
|
|
16
|
+
const { email, password } = req.body;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const user = await prisma.user.findUnique({
|
|
20
|
+
where: { email }
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!user) {
|
|
24
|
+
return res.render("auth/login", {
|
|
25
|
+
title: "Login",
|
|
26
|
+
error: "Invalid email or password"
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isValid = await verifyPassword(password, user.hashedPassword);
|
|
31
|
+
if (!isValid) {
|
|
32
|
+
return res.render("auth/login", {
|
|
33
|
+
title: "Login",
|
|
34
|
+
error: "Invalid email or password"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const token = signToken({ userId: user.id, email: user.email });
|
|
39
|
+
res.cookie("token", token, {
|
|
40
|
+
httpOnly: true,
|
|
41
|
+
secure: process.env.NODE_ENV === "production",
|
|
42
|
+
maxAge: 1000 * 60 * 60 * 24 // 24 hours
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
res.redirect("/");
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error("Login error:", err);
|
|
48
|
+
res.render("auth/login", {
|
|
49
|
+
title: "Login",
|
|
50
|
+
error: "An error occurred during login"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function register(req: Request, res: Response) {
|
|
56
|
+
const { email, password, confirmPassword } = req.body;
|
|
57
|
+
|
|
58
|
+
if (password !== confirmPassword) {
|
|
59
|
+
return res.render("auth/register", {
|
|
60
|
+
title: "Register",
|
|
61
|
+
error: "Passwords do not match"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const existingUser = await prisma.user.findUnique({
|
|
67
|
+
where: { email }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (existingUser) {
|
|
71
|
+
return res.render("auth/register", {
|
|
72
|
+
title: "Register",
|
|
73
|
+
error: "Email already in use"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const hashedPassword = await hashPassword(password);
|
|
78
|
+
|
|
79
|
+
await prisma.user.create({
|
|
80
|
+
data: {
|
|
81
|
+
email,
|
|
82
|
+
hashedPassword,
|
|
83
|
+
role: "user"
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
res.redirect("/login");
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error("Registration error:", err);
|
|
90
|
+
res.render("auth/register", {
|
|
91
|
+
title: "Register",
|
|
92
|
+
error: "An error occurred during registration"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function logout(req: Request, res: Response) {
|
|
98
|
+
res.clearCookie("token");
|
|
99
|
+
res.redirect("/login");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function requireAuth(req: Request, res: Response, next: any) {
|
|
103
|
+
const token = req.cookies?.token || req.headers.authorization?.split(" ")[1];
|
|
104
|
+
|
|
105
|
+
if (!token) {
|
|
106
|
+
return res.redirect("/login");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const decoded = verifyToken(token);
|
|
111
|
+
req.user = decoded;
|
|
112
|
+
next();
|
|
113
|
+
} catch {
|
|
114
|
+
res.clearCookie("token");
|
|
115
|
+
res.redirect("/login");
|
|
116
|
+
}
|
|
117
|
+
}
|