@programinglive/dev-workflow-mcp-server 1.7.11 → 1.7.13

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.
Files changed (39) hide show
  1. package/.dockerignore +60 -0
  2. package/.env.example +1 -0
  3. package/.env.production +16 -0
  4. package/CHANGELOG.md +35 -0
  5. package/Dockerfile +32 -0
  6. package/README.md +20 -0
  7. package/db/migrations/002_create_users_table.sql +22 -0
  8. package/db/migrations/003_create_workflow_history.sql +14 -0
  9. package/db/migrations/004_fix_workflow_history.sql +3 -0
  10. package/docker-compose.yml +24 -0
  11. package/docs/GCP_DEPLOYMENT.md +298 -0
  12. package/docs/product/PRD.md +1 -1
  13. package/docs/release-notes/RELEASE_NOTES.md +34 -0
  14. package/package.json +7 -2
  15. package/scripts/add-user.js +52 -0
  16. package/scripts/create-admin.js +47 -0
  17. package/scripts/deploy-gcloud.ps1 +160 -0
  18. package/scripts/deploy-gcloud.sh +154 -0
  19. package/scripts/health-check.js +50 -0
  20. package/scripts/test-history.js +56 -0
  21. package/web/__tests__/api/workflow-history.test.js +61 -0
  22. package/web/app/api/history-fallback/route.ts +56 -0
  23. package/web/app/docs/page.tsx +1 -1
  24. package/web/app/history/page.tsx +213 -0
  25. package/web/app/layout.tsx +1 -1
  26. package/web/app/login/page.tsx +144 -0
  27. package/web/app/page.tsx +0 -2
  28. package/web/components/Hero.tsx +1 -1
  29. package/web/components/History.tsx +63 -16
  30. package/web/components/Navbar.tsx +111 -9
  31. package/web/lib/auth.js +75 -0
  32. package/web/middleware/auth.js +26 -0
  33. package/web/next.config.ts +8 -1
  34. package/web/package-lock.json +239 -54
  35. package/web/package.json +10 -5
  36. package/web/scripts/db-query-history.js +120 -0
  37. package/web/scripts/smoke-test.js +30 -0
  38. package/web/server.js +171 -18
  39. package/verify-db-mysql.js +0 -92
@@ -2,22 +2,67 @@
2
2
 
3
3
  import { useState, useEffect } from "react";
4
4
  import Link from "next/link";
5
- import { Github, Menu, X, Moon, Sun } from "lucide-react";
5
+ import { Github, Menu, X, Moon, Sun, LogOut } from "lucide-react";
6
6
  import Image from "next/image";
7
7
 
8
8
  export default function Navbar() {
9
9
  const [isOpen, setIsOpen] = useState(false);
10
10
  const [theme, setTheme] = useState<"light" | "dark">("dark");
11
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
12
+ const [username, setUsername] = useState<string | null>(null);
11
13
 
12
14
  useEffect(() => {
13
15
  // Get theme from localStorage or system preference
14
16
  const savedTheme = localStorage.getItem("theme") as "light" | "dark" | null;
15
- if (savedTheme) {
16
- setTheme(savedTheme);
17
- } else {
18
- const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
19
- setTheme(prefersDark ? "dark" : "light");
20
- }
17
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
18
+
19
+ const initialTheme = savedTheme || (prefersDark ? "dark" : "light");
20
+ setTheme(initialTheme);
21
+
22
+ // Apply the class immediately
23
+ document.documentElement.classList.toggle("dark", initialTheme === "dark");
24
+
25
+ // Check authentication status
26
+ const checkAuth = () => {
27
+ // Check authentication status ONLY if we think we're logged in OR if we just received an update event
28
+ // This prevents 401 errors from cluttering the console for guests
29
+ const mightBeLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
30
+
31
+ if (mightBeLoggedIn) {
32
+ fetch('/api/auth/me')
33
+ .then(res => {
34
+ if (res.ok) {
35
+ return res.json();
36
+ }
37
+ throw new Error('Not authenticated');
38
+ })
39
+ .then(data => {
40
+ setIsAuthenticated(true);
41
+ setUsername(data.user?.username || 'User');
42
+ // Ensure local storage is in sync
43
+ localStorage.setItem('isLoggedIn', 'true');
44
+ })
45
+ .catch(() => {
46
+ // Session invalid/expired
47
+ localStorage.removeItem('isLoggedIn');
48
+ setIsAuthenticated(false);
49
+ setUsername(null);
50
+ });
51
+ } else {
52
+ setIsAuthenticated(false);
53
+ setUsername(null);
54
+ }
55
+ };
56
+
57
+ // Initial check
58
+ checkAuth();
59
+
60
+ // Listen for auth updates (login/logout/validated session)
61
+ window.addEventListener('authUpdate', checkAuth);
62
+
63
+ return () => {
64
+ window.removeEventListener('authUpdate', checkAuth);
65
+ };
21
66
  }, []);
22
67
 
23
68
  const toggleTheme = () => {
@@ -27,6 +72,19 @@ export default function Navbar() {
27
72
  document.documentElement.classList.toggle("dark", newTheme === "dark");
28
73
  };
29
74
 
75
+ const handleLogout = async () => {
76
+ try {
77
+ await fetch('/api/auth/logout', { method: 'POST' });
78
+ localStorage.removeItem('isLoggedIn');
79
+ setIsAuthenticated(false);
80
+ setUsername(null);
81
+ window.dispatchEvent(new Event('authUpdate'));
82
+ window.location.href = '/login';
83
+ } catch (error) {
84
+ console.error('Logout error:', error);
85
+ }
86
+ };
87
+
30
88
  return (
31
89
  <nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-[#0a0a0f]/80 backdrop-blur-lg border-b border-gray-200 dark:border-white/10 transition-colors duration-300">
32
90
  <div className="max-w-7xl mx-auto px-3 sm:px-4 lg:px-8">
@@ -37,7 +95,7 @@ export default function Navbar() {
37
95
  </Link>
38
96
  <div className="hidden md:flex items-center gap-3 lg:gap-6">
39
97
  <Link href="/#features" className="text-xs lg:text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition">Features</Link>
40
- <Link href="/#history" className="text-xs lg:text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition">History</Link>
98
+ <Link href="/history" className="text-xs lg:text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition">History</Link>
41
99
  <Link href="/#api" className="text-xs lg:text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition">API</Link>
42
100
  <Link href="/docs" className="text-xs lg:text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition">Docs</Link>
43
101
  <button
@@ -60,6 +118,28 @@ export default function Navbar() {
60
118
  <Github className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
61
119
  GitHub
62
120
  </a>
121
+ {isAuthenticated && (
122
+ <div className="flex items-center gap-4 pl-4 border-l border-gray-200 dark:border-white/10">
123
+ <span className="text-sm font-medium text-gray-900 dark:text-white">
124
+ {username}
125
+ </span>
126
+ <button
127
+ onClick={handleLogout}
128
+ className="flex items-center gap-1.5 px-3 py-1.5 lg:px-4 lg:py-2 bg-red-100 dark:bg-red-900/50 hover:bg-red-200 dark:hover:bg-red-800/50 rounded-lg transition text-xs lg:text-sm text-red-700 dark:text-red-300"
129
+ >
130
+ <LogOut className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
131
+ Logout
132
+ </button>
133
+ </div>
134
+ )}
135
+ {!isAuthenticated && (
136
+ <Link
137
+ href="/login"
138
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition"
139
+ >
140
+ Login
141
+ </Link>
142
+ )}
63
143
  </div>
64
144
  <div className="md:hidden flex items-center gap-2">
65
145
  <button
@@ -86,8 +166,13 @@ export default function Navbar() {
86
166
  {isOpen && (
87
167
  <div className="md:hidden border-t border-gray-200 dark:border-white/10 bg-white dark:bg-[#0a0a0f] transition-colors duration-300">
88
168
  <div className="px-4 py-4 space-y-3">
169
+ {isAuthenticated && (
170
+ <div className="px-3 py-2 text-sm font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-white/10 mb-2">
171
+ Signed in as {username}
172
+ </div>
173
+ )}
89
174
  <Link href="/#features" className="block text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition" onClick={() => setIsOpen(false)}>Features</Link>
90
- <Link href="/#history" className="block text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition" onClick={() => setIsOpen(false)}>History</Link>
175
+ <Link href="/history" className="block text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition" onClick={() => setIsOpen(false)}>History</Link>
91
176
  <Link href="/#api" className="block text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition" onClick={() => setIsOpen(false)}>API</Link>
92
177
  <Link href="/docs" className="block text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition" onClick={() => setIsOpen(false)}>Docs</Link>
93
178
  <a
@@ -99,6 +184,23 @@ export default function Navbar() {
99
184
  <Github className="w-4 h-4" />
100
185
  GitHub
101
186
  </a>
187
+ {isAuthenticated ? (
188
+ <button
189
+ onClick={() => { setIsOpen(false); handleLogout(); }}
190
+ className="w-full text-left flex items-center gap-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 px-2 py-1.5 rounded transition"
191
+ >
192
+ <LogOut className="w-4 h-4" />
193
+ Logout
194
+ </button>
195
+ ) : (
196
+ <Link
197
+ href="/login"
198
+ className="block w-full text-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition"
199
+ onClick={() => setIsOpen(false)}
200
+ >
201
+ Login
202
+ </Link>
203
+ )}
102
204
  </div>
103
205
  </div>
104
206
  )}
@@ -0,0 +1,75 @@
1
+ import bcrypt from 'bcrypt';
2
+ import pg from 'pg';
3
+
4
+ const SALT_ROUNDS = 10;
5
+
6
+ /**
7
+ * Hash a password using bcrypt
8
+ */
9
+ export async function hashPassword(password) {
10
+ return bcrypt.hash(password, SALT_ROUNDS);
11
+ }
12
+
13
+ /**
14
+ * Verify a password against a hash
15
+ */
16
+ export async function verifyPassword(password, hash) {
17
+ return bcrypt.compare(password, hash);
18
+ }
19
+
20
+ /**
21
+ * Create a new user
22
+ */
23
+ export async function createUser(pool, username, password, email = null) {
24
+ const passwordHash = await hashPassword(password);
25
+
26
+ const result = await pool.query(
27
+ 'INSERT INTO users (username, password_hash, email) VALUES ($1, $2, $3) RETURNING id, username, email, created_at',
28
+ [username, passwordHash, email]
29
+ );
30
+
31
+ return result.rows[0];
32
+ }
33
+
34
+ /**
35
+ * Authenticate a user
36
+ */
37
+ export async function authenticateUser(pool, username, password) {
38
+ const result = await pool.query(
39
+ 'SELECT id, username, password_hash, email FROM users WHERE username = $1',
40
+ [username]
41
+ );
42
+
43
+ if (result.rows.length === 0) {
44
+ return null;
45
+ }
46
+
47
+ const user = result.rows[0];
48
+ const isValid = await verifyPassword(password, user.password_hash);
49
+
50
+ if (!isValid) {
51
+ return null;
52
+ }
53
+
54
+ // Update last login
55
+ await pool.query(
56
+ 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1',
57
+ [user.id]
58
+ );
59
+
60
+ // Return user without password hash
61
+ const { password_hash, ...userWithoutPassword } = user;
62
+ return userWithoutPassword;
63
+ }
64
+
65
+ /**
66
+ * Get user by ID
67
+ */
68
+ export async function getUserById(pool, id) {
69
+ const result = await pool.query(
70
+ 'SELECT id, username, email, created_at, last_login FROM users WHERE id = $1',
71
+ [id]
72
+ );
73
+
74
+ return result.rows[0] || null;
75
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Middleware to require authentication
3
+ */
4
+ export function requireAuth(req, res, next) {
5
+ if (req.session && req.session.userId) {
6
+ return next();
7
+ }
8
+
9
+ // If it's an API request, return 401
10
+ if (req.path.startsWith('/api/')) {
11
+ return res.status(401).json({ error: 'Authentication required' });
12
+ }
13
+
14
+ // Otherwise redirect to login
15
+ res.redirect('/login');
16
+ }
17
+
18
+ /**
19
+ * Middleware to redirect if already authenticated
20
+ */
21
+ export function redirectIfAuthenticated(req, res, next) {
22
+ if (req.session && req.session.userId) {
23
+ return res.redirect('/');
24
+ }
25
+ next();
26
+ }
@@ -2,11 +2,18 @@ import type { NextConfig } from "next";
2
2
 
3
3
  const nextConfig: NextConfig = {
4
4
  // Static export for Netlify deployment
5
- output: 'export',
5
+ // Static export removed for server-side features
6
+ // output: 'export',
7
+ // Set turbopack root to silence multi-lockfile warning
8
+ turbopack: {
9
+ root: __dirname,
10
+ },
6
11
  // Disable image optimization for static export
7
12
  images: {
8
13
  unoptimized: true,
9
14
  },
15
+ // Exclude native modules from bundling
16
+ serverExternalPackages: ['pg'],
10
17
  };
11
18
 
12
19
  export default nextConfig;