added changes ready to ship
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
|
||||
async function handleRequest(req, res) {
|
||||
console.log("Handle called");
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
body: 'Hello from Spin Wasm (Corrected Export)!'
|
||||
};
|
||||
}
|
||||
|
||||
export const incomingHandler = {
|
||||
handle: handleRequest
|
||||
};
|
||||
|
||||
// Keep default just in case, but incomingHandler is key
|
||||
export default handleRequest;
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import { handleRequest } from '@fermyon/spin-sdk';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Middleware to mock Express request/response specific methods if needed
|
||||
// Or we just rewrite routes to use Hono context.
|
||||
|
||||
// Since rewriting all routes is heavy, let's try to mount simple wrappers
|
||||
// or just import the router logic if we refactor routes.
|
||||
// Given the implementation plan said "Re-implement routing logic",
|
||||
// and the routes are currently Express routers, we probably need to wrap them
|
||||
// or quickly rewrite them to Hono.
|
||||
|
||||
// Strategy: Import the routes and mount them.
|
||||
// BUT Express routers won't work in Hono.
|
||||
// We must rewrite the route definitions in this file or transformed files.
|
||||
// For "quick deployment", I will inline the mounting of existing logic where possible,
|
||||
// using the db_spin adapter.
|
||||
|
||||
import pool from './db_spin.js';
|
||||
import bcrypt from 'bcryptjs';
|
||||
// import { randomUUID } from 'crypto'; // Use global crypto
|
||||
const randomUUID = () => crypto.randomUUID();
|
||||
|
||||
// --- AUTH ROUTES ---
|
||||
app.post('/api/auth/login', async (c) => {
|
||||
try {
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) return c.json({ error: 'Email and password required' }, 400);
|
||||
|
||||
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
|
||||
if (rows.length === 0) return c.json({ error: 'Invalid email or password' }, 401);
|
||||
|
||||
const user = rows[0];
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!valid) return c.json({ error: 'Invalid email or password' }, 401);
|
||||
|
||||
return c.json({
|
||||
id: user.id, name: user.name, role: user.role, email: user.email,
|
||||
color: user.color, avatar: user.avatar, dept: user.dept,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/register', async (c) => {
|
||||
try {
|
||||
const { name, email, password, role, dept } = await c.req.json();
|
||||
if (!name || !email || !password) return c.json({ error: 'Required fields missing' }, 400);
|
||||
|
||||
const [existing] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
|
||||
if (existing.length > 0) return c.json({ error: 'Email already registered' }, 409);
|
||||
|
||||
const id = randomUUID();
|
||||
const password_hash = await bcrypt.hash(password, 10);
|
||||
// ... (simplified avatar logic)
|
||||
const avatar = name.substring(0, 2).toUpperCase();
|
||||
const color = '#818cf8';
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO users (id, name, role, email, password_hash, color, avatar, dept) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, name, role || 'employee', email, password_hash, color, avatar, dept || '']
|
||||
);
|
||||
return c.json({ id, name, role: role || 'employee', email, color, avatar, dept: dept || '' }, 201);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// --- TASKS ROUTES (Simplified for Wasm demo) ---
|
||||
async function getFullTask(taskId) {
|
||||
const [taskRows] = await pool.query('SELECT * FROM tasks WHERE id = ?', [taskId]);
|
||||
if (taskRows.length === 0) return null;
|
||||
const task = taskRows[0];
|
||||
// For brevity, not fetching sub-resources in this quick conversion,
|
||||
// but in full prod we would.
|
||||
// ... complete implementation would replicate existing logic ...
|
||||
return task;
|
||||
}
|
||||
|
||||
app.get('/api/tasks', async (c) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM tasks ORDER BY created_at DESC');
|
||||
// Simplify for now: Just return tasks
|
||||
return c.json(rows);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/tasks', async (c) => {
|
||||
try {
|
||||
const { title, description, status, priority, assignee, reporter, dueDate } = await c.req.json();
|
||||
const id = randomUUID();
|
||||
await pool.query(
|
||||
'INSERT INTO tasks (id, title, description, status, priority, assignee_id, reporter_id, due_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, title, description || '', status || 'todo', priority || 'medium', assignee || null, reporter || null, dueDate || null]
|
||||
);
|
||||
return c.json({ id, title, status }, 201);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (c) => c.json({ status: 'ok', engine: 'spin-wasm' }));
|
||||
|
||||
// Export the Spin handler
|
||||
export const spinHandler = async (req, res) => {
|
||||
// Spin generic handler interacting with Hono?
|
||||
// Hono has a fetch method: app.fetch(request, env, ctx)
|
||||
// Spin request is slightly different, but let's see if we can adapt.
|
||||
// Actually, Spin JS SDK v2 exports `handleRequest` which takes (request, response).
|
||||
// Hono might need an adapter.
|
||||
|
||||
// Simple adapter for Hono .fetch to Spin
|
||||
// Construct standard Request object from Spin calls if needed,
|
||||
// but simpler to use Hono's handle() if passing standard web standards.
|
||||
|
||||
// Assuming standard web signature is passed by recent Spin SDKs or we use 'node-adapter' if built via bundling.
|
||||
// But since we are likely using a bundler, strict Spin API is:
|
||||
// export async function handleRequest(request: Request): Promise<Response>
|
||||
|
||||
return app.fetch(req);
|
||||
};
|
||||
|
||||
// If using valid Spin JS plugin that looks for `handleRequest` as default export or named export
|
||||
// We will export it as `handleRequest` (default)
|
||||
export default async function (req) {
|
||||
return await app.fetch(req);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { build } from 'esbuild';
|
||||
import { SpinEsbuildPlugin } from "@spinframework/build-tools/plugins/esbuild/index.js";
|
||||
|
||||
const spinPlugin = await SpinEsbuildPlugin();
|
||||
|
||||
await build({
|
||||
entryPoints: ['./app_spin.js'],
|
||||
outfile: './dist/spin.js',
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
plugins: [spinPlugin],
|
||||
target: 'es2020',
|
||||
external: ['fermyon:*', 'spin:*'],
|
||||
});
|
||||
14
server/db.js
14
server/db.js
@@ -98,6 +98,20 @@ export async function initDB() {
|
||||
)
|
||||
`);
|
||||
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT,
|
||||
link VARCHAR(255),
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
console.log('✅ Database tables initialized');
|
||||
} finally {
|
||||
conn.release();
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { Mysql } from '@fermyon/spin-sdk';
|
||||
|
||||
const getEnv = (key, def) => {
|
||||
try {
|
||||
return (typeof process !== 'undefined' && process.env && process.env[key]) || def;
|
||||
} catch {
|
||||
return def;
|
||||
}
|
||||
};
|
||||
|
||||
const DB_URL = `mysql://${getEnv('DB_USER', 'root')}:${getEnv('DB_PASSWORD', 'scrumpass')}@${getEnv('DB_HOST', 'localhost')}:${getEnv('DB_PORT', '3306')}/${getEnv('DB_NAME', 'scrum_manager')}`;
|
||||
|
||||
function rowToObject(row, columns) {
|
||||
const obj = {};
|
||||
columns.forEach((col, index) => {
|
||||
obj[col.name] = row[index];
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
class SpinConnection {
|
||||
constructor(conn) {
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
async query(sql, params = []) {
|
||||
console.log('SpinDB Query:', sql, params);
|
||||
try {
|
||||
const result = this.conn.query(sql, params);
|
||||
|
||||
const rows = result.rows.map(r => rowToObject(r, result.columns));
|
||||
const fields = result.columns.map(c => ({ name: c.name }));
|
||||
|
||||
if (sql.trim().toUpperCase().startsWith('INSERT') || sql.trim().toUpperCase().startsWith('UPDATE') || sql.trim().toUpperCase().startsWith('DELETE')) {
|
||||
return [{ affectedRows: result.rowsAffected || 0, insertId: result.lastInsertId || 0 }, fields];
|
||||
}
|
||||
|
||||
return [rows, fields];
|
||||
} catch (e) {
|
||||
console.error('SpinDB Error:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async beginTransaction() {
|
||||
try {
|
||||
this.conn.query('START TRANSACTION', []);
|
||||
} catch (e) {
|
||||
console.warn('Transaction start failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async commit() {
|
||||
try { this.conn.query('COMMIT', []); } catch (e) { }
|
||||
}
|
||||
|
||||
async rollback() {
|
||||
try { this.conn.query('ROLLBACK', []); } catch (e) { }
|
||||
}
|
||||
|
||||
release() { }
|
||||
}
|
||||
|
||||
export const initDB = async () => {
|
||||
console.log('Spin DB adapter ready.');
|
||||
};
|
||||
|
||||
// Lazy initialization to avoid Wizer issues
|
||||
let poolInstance = null;
|
||||
function getPool() {
|
||||
if (!poolInstance) {
|
||||
poolInstance = {
|
||||
async getConnection() {
|
||||
const conn = Mysql.open(DB_URL);
|
||||
return new SpinConnection(conn);
|
||||
},
|
||||
async query(sql, params) {
|
||||
const conn = await this.getConnection();
|
||||
const result = await conn.query(sql, params);
|
||||
return result;
|
||||
},
|
||||
escape: (val) => `'${val}'`,
|
||||
end: () => { }
|
||||
};
|
||||
}
|
||||
return poolInstance;
|
||||
}
|
||||
|
||||
export default {
|
||||
query: (sql, params) => getPool().query(sql, params),
|
||||
getConnection: () => getPool().getConnection(),
|
||||
end: () => getPool().end(),
|
||||
initDB
|
||||
};
|
||||
@@ -4,31 +4,57 @@ import { initDB } from './db.js';
|
||||
import authRoutes from './routes/auth.js';
|
||||
import taskRoutes from './routes/tasks.js';
|
||||
import exportRoutes from './routes/export.js';
|
||||
import notificationRoutes from './routes/notifications.js';
|
||||
|
||||
import { createServer } from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Socket.io connection handling
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('join', (userId) => {
|
||||
socket.join(userId);
|
||||
console.log(`User ${userId} joined notification room`);
|
||||
});
|
||||
});
|
||||
|
||||
// Middleware to attach io to req
|
||||
app.use((req, res, next) => {
|
||||
req.io = io;
|
||||
next();
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/tasks', taskRoutes);
|
||||
app.use('/api/export', exportRoutes);
|
||||
app.use('/api/notifications', notificationRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Initialize DB and start server
|
||||
// Initialize DB and start server
|
||||
async function start() {
|
||||
try {
|
||||
await initDB();
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Backend server running on port ${PORT}`);
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`🚀 Backend server running on port ${PORT} with Socket.io`);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -41,4 +67,4 @@ if (process.env.NODE_ENV !== 'test') {
|
||||
start();
|
||||
}
|
||||
|
||||
export { app, start };
|
||||
export { app, start, io };
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"packages": {
|
||||
"@fermyon/spin-sdk": {
|
||||
"witPath": "../../bin/wit",
|
||||
"world": "spin-imports"
|
||||
}
|
||||
},
|
||||
"project": {},
|
||||
"version": 1
|
||||
}
|
||||
3139
server/package-lock.json
generated
3139
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,21 +5,16 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "vitest",
|
||||
"build:spin": "node build.mjs && node node_modules/@fermyon/spin-sdk/bin/j2w.mjs -i dist/spin.js -o dist/main.wasm --trigger-type spin3-http"
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fermyon/spin-sdk": "^2.2.0",
|
||||
"@spinframework/wasi-http-proxy": "^1.0.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"hono": "^4.6.14",
|
||||
"mysql2": "^3.14.1"
|
||||
"mysql2": "^3.14.1",
|
||||
"socket.io": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@spinframework/build-tools": "^1.0.7",
|
||||
"esbuild": "^0.24.2",
|
||||
"supertest": "^7.2.2",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Polyfill for crypto in Wizer environment
|
||||
if (!globalThis.crypto) {
|
||||
globalThis.crypto = {
|
||||
getRandomValues: (buffer) => {
|
||||
// Check if buffer is valid
|
||||
if (!buffer || typeof buffer.length !== 'number') {
|
||||
throw new Error("crypto.getRandomValues: invalid buffer");
|
||||
}
|
||||
// Fill with pseudo-random numbers
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
buffer[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return buffer;
|
||||
},
|
||||
randomUUID: () => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
};
|
||||
console.log("Polyfilled globalThis.crypto for Wizer/Spin");
|
||||
}
|
||||
51
server/routes/notifications.js
Normal file
51
server/routes/notifications.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Router } from 'express';
|
||||
import pool from '../db.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/notifications/:userId
|
||||
router.get('/:userId', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 50',
|
||||
[req.params.userId]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error('Fetch notifications error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/notifications/:id/read
|
||||
router.put('/:id/read', async (req, res) => {
|
||||
try {
|
||||
await pool.query('UPDATE notifications SET is_read = TRUE WHERE id = ?', [req.params.id]);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Mark notification read error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Helper: Create notification and emit if possible
|
||||
export async function createNotification(req, { userId, type, title, message, link }) {
|
||||
try {
|
||||
const id = randomUUID();
|
||||
await pool.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[id, userId, type, title, message, link]
|
||||
);
|
||||
|
||||
if (req.io) {
|
||||
req.io.to(userId).emit('notification', {
|
||||
id, user_id: userId, type, title, message, link, is_read: false, created_at: new Date()
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Create notification error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import pool from '../db.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { createNotification } from './notifications.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -88,6 +89,17 @@ router.post('/', async (req, res) => {
|
||||
[actId, id, '📝 Task created']);
|
||||
|
||||
await conn.commit();
|
||||
|
||||
if (assignee) {
|
||||
await createNotification(req, {
|
||||
userId: assignee,
|
||||
type: 'assignment',
|
||||
title: 'New Task Assigned',
|
||||
message: `You have been assigned to task: ${title}`,
|
||||
link: `/tasks?id=${id}`
|
||||
});
|
||||
}
|
||||
|
||||
const task = await getFullTask(id);
|
||||
res.status(201).json(task);
|
||||
} catch (err) {
|
||||
@@ -178,6 +190,24 @@ router.post('/:id/comments', async (req, res) => {
|
||||
const timestamp = new Date();
|
||||
await pool.query('INSERT INTO comments (id, task_id, user_id, text, timestamp) VALUES (?, ?, ?, ?, ?)',
|
||||
[id, req.params.id, userId, text, timestamp]);
|
||||
|
||||
// Mention detection: @[Name](userId)
|
||||
const mentions = text.match(/@\[([^\]]+)\]\(([^)]+)\)/g);
|
||||
if (mentions) {
|
||||
const mentionedUserIds = [...new Set(mentions.map(m => m.match(/\(([^)]+)\)/)[1]))];
|
||||
for (const mId of mentionedUserIds) {
|
||||
if (mId !== userId) {
|
||||
await createNotification(req, {
|
||||
userId: mId,
|
||||
type: 'mention',
|
||||
title: 'New Mention',
|
||||
message: `You were mentioned in a comment on task ${req.params.id}`,
|
||||
link: `/tasks?id=${req.params.id}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({ id, userId, text, timestamp: timestamp.toISOString() });
|
||||
} catch (err) {
|
||||
console.error('Add comment error:', err);
|
||||
|
||||
Reference in New Issue
Block a user