@erwininteractive/mvc 0.6.3 → 0.6.7

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 CHANGED
@@ -16,11 +16,12 @@ program
16
16
  .version("0.2.0")
17
17
  .addHelpText("after", `
18
18
  Examples:
19
- $ erwinmvc init myapp Create a new app
20
- $ erwinmvc generate resource Post Generate CRUD for Post
21
- $ erwinmvc webauthn Setup passkey authentication
22
- $ erwinmvc make:auth Generate authentication system
23
- $ erwinmvc list:routes Show all routes
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);
@@ -0,0 +1,34 @@
1
+ import type { ZodError, ZodSchema } from "zod";
2
+ export type ValidationStrategyType = "redirect" | "json";
3
+ /**
4
+ * Zod validation middleware
5
+ * @param schema - Zod schema to validate against
6
+ * @param strategy - 'redirect' (web) or 'json' (API)
7
+ */
8
+ export declare function validate<T>(schema: ZodSchema<T>, strategy?: ValidationStrategyType): (req: any, res: any, next: any) => Promise<any>;
9
+ /**
10
+ * Extract field errors from Zod ZodError
11
+ */
12
+ export declare function getFieldErrors(error: ZodError): Array<{
13
+ field: string;
14
+ message: string;
15
+ }>;
16
+ /**
17
+ * Preserves form input when validation fails
18
+ */
19
+ export declare function getOldInput(req: any): any;
20
+ /**
21
+ * Get validation errors for display
22
+ */
23
+ export declare function getErrors(req: any): Array<{
24
+ field: string;
25
+ message: string;
26
+ }>;
27
+ /**
28
+ * Check if a field has errors
29
+ */
30
+ export declare function hasFieldError(field: string, errors: any): boolean;
31
+ /**
32
+ * Get error message for a specific field
33
+ */
34
+ export declare function getFieldError(field: string, errors: any): string | undefined;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validate = validate;
4
+ exports.getFieldErrors = getFieldErrors;
5
+ exports.getOldInput = getOldInput;
6
+ exports.getErrors = getErrors;
7
+ exports.hasFieldError = hasFieldError;
8
+ exports.getFieldError = getFieldError;
9
+ /**
10
+ * Zod validation middleware
11
+ * @param schema - Zod schema to validate against
12
+ * @param strategy - 'redirect' (web) or 'json' (API)
13
+ */
14
+ function validate(schema, strategy = "redirect") {
15
+ return async (req, res, next) => {
16
+ const result = await schema.safeParseAsync(req.body);
17
+ if (!result.success) {
18
+ const errors = getFieldErrors(result.error);
19
+ const oldInput = req.body;
20
+ if (strategy === "json") {
21
+ return res.status(422).json({ errors, oldInput });
22
+ }
23
+ // Redirect with errors and old input for web forms
24
+ req.flash("errors", errors);
25
+ req.flash("oldInput", oldInput);
26
+ // Get referrer URL or use default
27
+ const redirectUrl = req.headers.referer || req.headers.referrer || "/";
28
+ return res.redirect(redirectUrl);
29
+ }
30
+ req.validatedBody = result.data;
31
+ next();
32
+ };
33
+ }
34
+ /**
35
+ * Extract field errors from Zod ZodError
36
+ */
37
+ function getFieldErrors(error) {
38
+ return error.errors.map((err) => ({
39
+ field: err.path.join("."),
40
+ message: err.message,
41
+ }));
42
+ }
43
+ /**
44
+ * Preserves form input when validation fails
45
+ */
46
+ function getOldInput(req) {
47
+ return req.flash("oldInput")[0] || {};
48
+ }
49
+ /**
50
+ * Get validation errors for display
51
+ */
52
+ function getErrors(req) {
53
+ return req.flash("errors") || [];
54
+ }
55
+ /**
56
+ * Check if a field has errors
57
+ */
58
+ function hasFieldError(field, errors) {
59
+ return errors.some((err) => err.field === field);
60
+ }
61
+ /**
62
+ * Get error message for a specific field
63
+ */
64
+ function getFieldError(field, errors) {
65
+ const error = errors.find((err) => err.field === field);
66
+ return error?.message;
67
+ }
@@ -3,4 +3,5 @@ export type { MvcAppOptions, MvcApp } from "./App";
3
3
  export { getPrismaClient, disconnectPrisma } from "./db";
4
4
  export { hashPassword, verifyPassword, signToken, verifyToken, authenticate, } from "./Auth";
5
5
  export { startRegistration, completeRegistration, startAuthentication, completeAuthentication, getRPConfig, } from "./WebAuthn";
6
+ export { validate, getFieldErrors, getErrors, getOldInput } from "./Validation";
6
7
  export { registerControllers, registerController } from "./Router";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerController = exports.registerControllers = exports.getRPConfig = exports.completeAuthentication = exports.startAuthentication = exports.completeRegistration = exports.startRegistration = exports.authenticate = exports.verifyToken = exports.signToken = exports.verifyPassword = exports.hashPassword = exports.disconnectPrisma = exports.getPrismaClient = exports.startServer = exports.createMvcApp = void 0;
3
+ exports.registerController = exports.registerControllers = exports.getOldInput = exports.getErrors = exports.getFieldErrors = exports.validate = exports.getRPConfig = exports.completeAuthentication = exports.startAuthentication = exports.completeRegistration = exports.startRegistration = exports.authenticate = exports.verifyToken = exports.signToken = exports.verifyPassword = exports.hashPassword = exports.disconnectPrisma = exports.getPrismaClient = exports.startServer = exports.createMvcApp = void 0;
4
4
  // App factory and server
5
5
  var App_1 = require("./App");
6
6
  Object.defineProperty(exports, "createMvcApp", { enumerable: true, get: function () { return App_1.createMvcApp; } });
@@ -23,6 +23,12 @@ Object.defineProperty(exports, "completeRegistration", { enumerable: true, get:
23
23
  Object.defineProperty(exports, "startAuthentication", { enumerable: true, get: function () { return WebAuthn_1.startAuthentication; } });
24
24
  Object.defineProperty(exports, "completeAuthentication", { enumerable: true, get: function () { return WebAuthn_1.completeAuthentication; } });
25
25
  Object.defineProperty(exports, "getRPConfig", { enumerable: true, get: function () { return WebAuthn_1.getRPConfig; } });
26
+ // Validation
27
+ var Validation_1 = require("./Validation");
28
+ Object.defineProperty(exports, "validate", { enumerable: true, get: function () { return Validation_1.validate; } });
29
+ Object.defineProperty(exports, "getFieldErrors", { enumerable: true, get: function () { return Validation_1.getFieldErrors; } });
30
+ Object.defineProperty(exports, "getErrors", { enumerable: true, get: function () { return Validation_1.getErrors; } });
31
+ Object.defineProperty(exports, "getOldInput", { enumerable: true, get: function () { return Validation_1.getOldInput; } });
26
32
  // Routing
27
33
  var Router_1 = require("./Router");
28
34
  Object.defineProperty(exports, "registerControllers", { enumerable: true, get: function () { return Router_1.registerControllers; } });
@@ -2,6 +2,7 @@ export interface InitOptions {
2
2
  skipInstall?: boolean;
3
3
  withDatabase?: boolean;
4
4
  withCi?: boolean;
5
+ withTailwind?: boolean;
5
6
  }
6
7
  /**
7
8
  * Scaffold a new MVC application.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erwininteractive/mvc",
3
- "version": "0.6.3",
3
+ "version": "0.6.7",
4
4
  "description": "A lightweight, full-featured MVC framework for Node.js with Express, Prisma, and EJS",
5
5
  "main": "dist/framework/index.js",
6
6
  "types": "dist/framework/index.d.ts",
@@ -52,7 +52,8 @@
52
52
  "helmet": "^8.0.0",
53
53
  "jsonwebtoken": "^9.0.2",
54
54
  "prisma": "^6.0.0",
55
- "redis": "^4.7.0"
55
+ "redis": "^4.7.0",
56
+ "zod": "^3.23.8"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/bcryptjs": "^2.4.6",
@@ -12,11 +12,13 @@
12
12
  "db:push": "prisma db push"
13
13
  },
14
14
  "dependencies": {
15
- "@erwininteractive/mvc": "^0.4.0",
16
- "cookie-parser": "^1.4.6"
15
+ "@erwininteractive/mvc": "^0.6.0",
16
+ "cookie-parser": "^1.4.6",
17
+ "zod": "^3.23.8"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@types/cookie-parser": "^1.4.7",
21
+ "@types/ejs": "^3.1.5",
20
22
  "@types/express": "^5.0.0",
21
23
  "@types/node": "^22.7.5",
22
24
  "tsx": "^4.19.1",
@@ -0,0 +1,9 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ autoprefixer: {},
6
+ },
7
+ };
8
+
9
+ export default config;
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -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
+ }