FRE-5256: Review silent active run for Senior Engineer - false positive
- Senior Engineer run 8f0979ee on FRE-4807 silent for 1h (suspicious threshold) - Run was automation/system triggered after pending ci.yml security fixes were already completed by CTO at 19:07 UTC - Zero output sequences because run had no actionable scope - FRE-5256 marked done with false positive disposition - FRE-4807 reassigned to Security Reviewer for ci.yml re-review Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
142
nessa-api/src/config/database.js
Normal file
142
nessa-api/src/config/database.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const db = new Database(join(__dirname, '../data/nessa.db'));
|
||||
|
||||
// Enable foreign keys
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
// Initialize database schema
|
||||
function initializeSchema() {
|
||||
// Users table (simplified - in production, use auth service)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// Clubs table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS clubs (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT,
|
||||
creator_id TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (creator_id) REFERENCES users(id)
|
||||
);
|
||||
`);
|
||||
|
||||
// Club memberships
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS club_memberships (
|
||||
club_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
role TEXT DEFAULT 'member',
|
||||
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (club_id, user_id),
|
||||
FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Challenges table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS challenges (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
type TEXT NOT NULL,
|
||||
start_date DATETIME,
|
||||
end_date DATETIME,
|
||||
creator_id TEXT NOT NULL,
|
||||
club_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (creator_id) REFERENCES users(id),
|
||||
FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE SET NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// Challenge participants
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS challenge_participants (
|
||||
challenge_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'active',
|
||||
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (challenge_id, user_id),
|
||||
FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Challenge submissions
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS challenge_submissions (
|
||||
id TEXT PRIMARY KEY,
|
||||
challenge_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
proof TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Posts table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
type TEXT DEFAULT 'text',
|
||||
club_id TEXT,
|
||||
challenge_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE SET NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// Likes table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS likes (
|
||||
post_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (post_id, user_id),
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Comments table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id TEXT PRIMARY KEY,
|
||||
post_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
console.log('Database schema initialized');
|
||||
}
|
||||
|
||||
initializeSchema();
|
||||
|
||||
export default db;
|
||||
61
nessa-api/src/index.js
Normal file
61
nessa-api/src/index.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
import clubsRoutes from './routes/clubs.js';
|
||||
import challengesRoutes from './routes/challenges.js';
|
||||
import socialRoutes from './routes/social.js';
|
||||
import healthRoutes from './routes/health.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 8087;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Request logging middleware
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
console.log(`${req.method} ${req.path} ${res.statusCode} (${duration}ms)`);
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api/health', healthRoutes);
|
||||
app.use('/api/clubs', clubsRoutes);
|
||||
app.use('/api/challenges', challengesRoutes);
|
||||
app.use('/api/social', socialRoutes);
|
||||
|
||||
// 404 handler
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: 'Not found' });
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Error:', err);
|
||||
res.status(err.status || 500).json({
|
||||
error: err.message || 'Internal server error'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`Nessa API server running on port ${PORT}`);
|
||||
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
export { server };
|
||||
157
nessa-api/src/models/Challenge.js
Normal file
157
nessa-api/src/models/Challenge.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import db from '../config/database.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class Challenge {
|
||||
static getAll(filters = {}) {
|
||||
let query = `
|
||||
SELECT c.*, u.username as creator_username, cl.name as club_name
|
||||
FROM challenges c
|
||||
LEFT JOIN users u ON c.creator_id = u.id
|
||||
LEFT JOIN clubs cl ON c.club_id = cl.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (filters.type) {
|
||||
query += ' AND c.type = ?';
|
||||
params.push(filters.type);
|
||||
}
|
||||
|
||||
if (filters.clubId) {
|
||||
query += ' AND c.club_id = ?';
|
||||
params.push(filters.clubId);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
if (filters.status === 'active') {
|
||||
query += ' AND (c.end_date IS NULL OR c.end_date > ?)';
|
||||
params.push(new Date().toISOString());
|
||||
} else if (filters.status === 'completed') {
|
||||
query += ' AND c.end_date < ?';
|
||||
params.push(new Date().toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
query += ' ORDER BY c.created_at DESC';
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
return stmt.all(...params);
|
||||
}
|
||||
|
||||
static getById(id) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, u.username as creator_username, cl.name as club_name
|
||||
FROM challenges c
|
||||
LEFT JOIN users u ON c.creator_id = u.id
|
||||
LEFT JOIN clubs cl ON c.club_id = cl.id
|
||||
WHERE c.id = ?
|
||||
`);
|
||||
return stmt.get(id);
|
||||
}
|
||||
|
||||
static create({ title, description, type, startDate, endDate, creatorId, clubId }) {
|
||||
const id = uuidv4();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO challenges (id, title, description, type, start_date, end_date, creator_id, club_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(id, title, description || null, type, startDate || null, endDate || null, creatorId, clubId || null);
|
||||
return this.getById(id);
|
||||
}
|
||||
|
||||
static update(id, data) {
|
||||
const challenge = this.getById(id);
|
||||
if (!challenge) return null;
|
||||
|
||||
const allowedFields = ['title', 'description', 'type', 'startDate', 'endDate'];
|
||||
const updates = [];
|
||||
const values = [];
|
||||
|
||||
const fieldMap = { startDate: 'start_date', endDate: 'end_date' };
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (data[field] !== undefined) {
|
||||
const dbField = fieldMap[field] || field;
|
||||
updates.push(`${dbField} = ?`);
|
||||
values.push(data[field]);
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.length === 0) return challenge;
|
||||
|
||||
values.push(id);
|
||||
const stmt = db.prepare(`
|
||||
UPDATE challenges SET ${updates.join(', ')} WHERE id = ?
|
||||
`);
|
||||
stmt.run(...values);
|
||||
|
||||
return this.getById(id);
|
||||
}
|
||||
|
||||
static delete(id) {
|
||||
const stmt = db.prepare('DELETE FROM challenges WHERE id = ?');
|
||||
const result = stmt.run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static getParticipants(challengeId) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT cp.*, u.username, u.display_name, u.avatar_url
|
||||
FROM challenge_participants cp
|
||||
JOIN users u ON cp.user_id = u.id
|
||||
WHERE cp.challenge_id = ?
|
||||
ORDER BY cp.joined_at ASC
|
||||
`);
|
||||
return stmt.all(challengeId);
|
||||
}
|
||||
|
||||
static addParticipant(challengeId, userId) {
|
||||
const existing = db.prepare(`
|
||||
SELECT * FROM challenge_participants WHERE challenge_id = ? AND user_id = ?
|
||||
`).get(challengeId, userId);
|
||||
|
||||
if (existing) {
|
||||
throw new Error('Already a participant');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO challenge_participants (challenge_id, user_id)
|
||||
VALUES (?, ?)
|
||||
`);
|
||||
stmt.run(challengeId, userId);
|
||||
|
||||
return { challengeId, userId, status: 'active', joinedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
static submitProgress(challengeId, { userId, data, proof }) {
|
||||
const id = uuidv4();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO challenge_submissions (id, challenge_id, user_id, data, proof)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(id, challengeId, userId, JSON.stringify(data), proof || null);
|
||||
|
||||
return {
|
||||
id,
|
||||
challengeId,
|
||||
userId,
|
||||
data,
|
||||
proof,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
static getSubmissions(challengeId) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM challenge_submissions
|
||||
WHERE challenge_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
return stmt.all(challengeId).map(s => ({
|
||||
...s,
|
||||
data: JSON.parse(s.data)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export default Challenge;
|
||||
106
nessa-api/src/models/Club.js
Normal file
106
nessa-api/src/models/Club.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import db from '../config/database.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class Club {
|
||||
static getAll() {
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, u.username as creator_username
|
||||
FROM clubs c
|
||||
LEFT JOIN users u ON c.creator_id = u.id
|
||||
ORDER BY c.created_at DESC
|
||||
`);
|
||||
return stmt.all();
|
||||
}
|
||||
|
||||
static getById(id) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, u.username as creator_username
|
||||
FROM clubs c
|
||||
LEFT JOIN users u ON c.creator_id = u.id
|
||||
WHERE c.id = ?
|
||||
`);
|
||||
return stmt.get(id);
|
||||
}
|
||||
|
||||
static create({ name, description, category, creatorId }) {
|
||||
const id = uuidv4();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO clubs (id, name, description, category, creator_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(id, name, description || null, category || null, creatorId);
|
||||
return this.getById(id);
|
||||
}
|
||||
|
||||
static update(id, data) {
|
||||
const club = this.getById(id);
|
||||
if (!club) return null;
|
||||
|
||||
const allowedFields = ['name', 'description', 'category'];
|
||||
const updates = [];
|
||||
const values = [];
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (data[field] !== undefined) {
|
||||
updates.push(`${field} = ?`);
|
||||
values.push(data[field]);
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.length === 0) return club;
|
||||
|
||||
values.push(id);
|
||||
const stmt = db.prepare(`
|
||||
UPDATE clubs SET ${updates.join(', ')} WHERE id = ?
|
||||
`);
|
||||
stmt.run(...values);
|
||||
|
||||
return this.getById(id);
|
||||
}
|
||||
|
||||
static delete(id) {
|
||||
const stmt = db.prepare('DELETE FROM clubs WHERE id = ?');
|
||||
const result = stmt.run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static getMembers(clubId) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT cm.*, u.username, u.display_name, u.avatar_url
|
||||
FROM club_memberships cm
|
||||
JOIN users u ON cm.user_id = u.id
|
||||
WHERE cm.club_id = ?
|
||||
ORDER BY cm.joined_at ASC
|
||||
`);
|
||||
return stmt.all(clubId);
|
||||
}
|
||||
|
||||
static addMember(clubId, userId) {
|
||||
// Check if already a member
|
||||
const existing = db.prepare(`
|
||||
SELECT * FROM club_memberships WHERE club_id = ? AND user_id = ?
|
||||
`).get(clubId, userId);
|
||||
|
||||
if (existing) {
|
||||
throw new Error('Already a member');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO club_memberships (club_id, user_id)
|
||||
VALUES (?, ?)
|
||||
`);
|
||||
stmt.run(clubId, userId);
|
||||
|
||||
return { clubId, userId, role: 'member', joinedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
static removeMember(clubId, userId) {
|
||||
const stmt = db.prepare(`
|
||||
DELETE FROM club_memberships WHERE club_id = ? AND user_id = ?
|
||||
`);
|
||||
const result = stmt.run(clubId, userId);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default Club;
|
||||
137
nessa-api/src/models/Social.js
Normal file
137
nessa-api/src/models/Social.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import db from '../config/database.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class Social {
|
||||
static getFeed(userId, { limit = 20, offset = 0 } = {}) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
p.*,
|
||||
u.username,
|
||||
u.display_name,
|
||||
u.avatar_url,
|
||||
cl.name as club_name,
|
||||
ch.title as challenge_title,
|
||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as like_count,
|
||||
(SELECT COUNT(*) FROM comments WHERE post_id = p.id) as comment_count,
|
||||
(SELECT COUNT(*) FROM likes WHERE post_id = p.id AND user_id = ?) as user_liked
|
||||
FROM posts p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
LEFT JOIN clubs cl ON p.club_id = cl.id
|
||||
LEFT JOIN challenges ch ON p.challenge_id = ch.id
|
||||
WHERE p.user_id IN (
|
||||
SELECT user_id FROM club_memberships WHERE club_id IN (
|
||||
SELECT club_id FROM club_memberships WHERE user_id = ?
|
||||
)
|
||||
UNION
|
||||
SELECT ?
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`);
|
||||
|
||||
return stmt.all(userId, userId, userId, limit, offset);
|
||||
}
|
||||
|
||||
static createPost({ userId, content, type, clubId, challengeId }) {
|
||||
const id = uuidv4();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO posts (id, user_id, content, type, club_id, challenge_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(id, userId, content, type || 'text', clubId || null, challengeId || null);
|
||||
|
||||
return this.getPost(id);
|
||||
}
|
||||
|
||||
static getPost(id) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
p.*,
|
||||
u.username,
|
||||
u.display_name,
|
||||
u.avatar_url,
|
||||
cl.name as club_name,
|
||||
ch.title as challenge_title
|
||||
FROM posts p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
LEFT JOIN clubs cl ON p.club_id = cl.id
|
||||
LEFT JOIN challenges ch ON p.challenge_id = ch.id
|
||||
WHERE p.id = ?
|
||||
`);
|
||||
return stmt.get(id);
|
||||
}
|
||||
|
||||
static deletePost(id) {
|
||||
const stmt = db.prepare('DELETE FROM posts WHERE id = ?');
|
||||
const result = stmt.run(id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static likePost(postId, userId) {
|
||||
const existing = db.prepare(`
|
||||
SELECT * FROM likes WHERE post_id = ? AND user_id = ?
|
||||
`).get(postId, userId);
|
||||
|
||||
if (existing) {
|
||||
throw new Error('Already liked');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO likes (post_id, user_id)
|
||||
VALUES (?, ?)
|
||||
`);
|
||||
stmt.run(postId, userId);
|
||||
|
||||
return { postId, userId, createdAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
static unlikePost(postId, userId) {
|
||||
const stmt = db.prepare(`
|
||||
DELETE FROM likes WHERE post_id = ? AND user_id = ?
|
||||
`);
|
||||
const result = stmt.run(postId, userId);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
static addComment(postId, { userId, content }) {
|
||||
const id = uuidv4();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO comments (id, post_id, user_id, content)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(id, postId, userId, content);
|
||||
|
||||
return this.getComment(id);
|
||||
}
|
||||
|
||||
static getComment(id) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
c.*,
|
||||
u.username,
|
||||
u.display_name,
|
||||
u.avatar_url
|
||||
FROM comments c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
WHERE c.id = ?
|
||||
`);
|
||||
return stmt.get(id);
|
||||
}
|
||||
|
||||
static getComments(postId) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
c.*,
|
||||
u.username,
|
||||
u.display_name,
|
||||
u.avatar_url
|
||||
FROM comments c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
WHERE c.post_id = ?
|
||||
ORDER BY c.created_at ASC
|
||||
`);
|
||||
return stmt.all(postId);
|
||||
}
|
||||
}
|
||||
|
||||
export default Social;
|
||||
115
nessa-api/src/routes/challenges.js
Normal file
115
nessa-api/src/routes/challenges.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Router } from 'express';
|
||||
import Challenge from '../models/Challenge.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/challenges - List all challenges
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const challenges = Challenge.getAll(req.query);
|
||||
res.json(challenges);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/challenges/:id - Get a specific challenge
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const challenge = Challenge.getById(req.params.id);
|
||||
if (!challenge) {
|
||||
return res.status(404).json({ error: 'Challenge not found' });
|
||||
}
|
||||
res.json(challenge);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/challenges - Create a new challenge
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { title, description, type, startDate, endDate, creatorId, clubId } = req.body;
|
||||
|
||||
if (!title || !creatorId) {
|
||||
return res.status(400).json({ error: 'title and creatorId are required' });
|
||||
}
|
||||
|
||||
const challenge = Challenge.create({
|
||||
title, description, type, startDate, endDate, creatorId, clubId
|
||||
});
|
||||
res.status(201).json(challenge);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/challenges/:id - Update a challenge
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const challenge = Challenge.update(req.params.id, req.body);
|
||||
if (!challenge) {
|
||||
return res.status(404).json({ error: 'Challenge not found' });
|
||||
}
|
||||
res.json(challenge);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/challenges/:id - Delete a challenge
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const deleted = Challenge.delete(req.params.id);
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ error: 'Challenge not found' });
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/challenges/:id/participants - Get challenge participants
|
||||
router.get('/:id/participants', (req, res) => {
|
||||
try {
|
||||
const participants = Challenge.getParticipants(req.params.id);
|
||||
res.json(participants);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/challenges/:id/participants - Join a challenge
|
||||
router.post('/:id/participants', (req, res) => {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'userId is required' });
|
||||
}
|
||||
|
||||
const participation = Challenge.addParticipant(req.params.id, userId);
|
||||
res.status(201).json(participation);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/challenges/:id/submissions - Submit challenge progress
|
||||
router.post('/:id/submissions', (req, res) => {
|
||||
try {
|
||||
const { userId, data, proof } = req.body;
|
||||
|
||||
if (!userId || !data) {
|
||||
return res.status(400).json({ error: 'userId and data are required' });
|
||||
}
|
||||
|
||||
const submission = Challenge.submitProgress(req.params.id, { userId, data, proof });
|
||||
res.status(201).json(submission);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
97
nessa-api/src/routes/clubs.js
Normal file
97
nessa-api/src/routes/clubs.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Router } from 'express';
|
||||
import Club from '../models/Club.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/clubs - List all clubs
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const clubs = Club.getAll();
|
||||
res.json(clubs);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/clubs/:id - Get a specific club
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const club = Club.getById(req.params.id);
|
||||
if (!club) {
|
||||
return res.status(404).json({ error: 'Club not found' });
|
||||
}
|
||||
res.json(club);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/clubs - Create a new club
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { name, description, category, creatorId } = req.body;
|
||||
|
||||
if (!name || !creatorId) {
|
||||
return res.status(400).json({ error: 'name and creatorId are required' });
|
||||
}
|
||||
|
||||
const club = Club.create({ name, description, category, creatorId });
|
||||
res.status(201).json(club);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/clubs/:id - Update a club
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const club = Club.update(req.params.id, req.body);
|
||||
if (!club) {
|
||||
return res.status(404).json({ error: 'Club not found' });
|
||||
}
|
||||
res.json(club);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/clubs/:id - Delete a club
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const deleted = Club.delete(req.params.id);
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ error: 'Club not found' });
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/clubs/:id/members - Get club members
|
||||
router.get('/:id/members', (req, res) => {
|
||||
try {
|
||||
const members = Club.getMembers(req.params.id);
|
||||
res.json(members);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/clubs/:id/members - Join a club
|
||||
router.post('/:id/members', (req, res) => {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'userId is required' });
|
||||
}
|
||||
|
||||
const membership = Club.addMember(req.params.id, userId);
|
||||
res.status(201).json(membership);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
23
nessa-api/src/routes/health.js
Normal file
23
nessa-api/src/routes/health.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'nessa-api',
|
||||
version: '1.0.0',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/ready', (req, res) => {
|
||||
// Check database connection, external services, etc.
|
||||
res.json({ ready: true });
|
||||
});
|
||||
|
||||
router.get('/live', (req, res) => {
|
||||
res.json({ alive: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
128
nessa-api/src/routes/social.js
Normal file
128
nessa-api/src/routes/social.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Router } from 'express';
|
||||
import Social from '../models/Social.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/social/feed - Get user's social feed
|
||||
router.get('/feed', (req, res) => {
|
||||
try {
|
||||
const { userId, limit = 20, offset = 0 } = req.query;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'userId is required' });
|
||||
}
|
||||
|
||||
const feed = Social.getFeed(userId, { limit: parseInt(limit), offset: parseInt(offset) });
|
||||
res.json(feed);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/social/posts - Create a new post
|
||||
router.post('/posts', (req, res) => {
|
||||
try {
|
||||
const { userId, content, type, clubId, challengeId } = req.body;
|
||||
|
||||
if (!userId || !content) {
|
||||
return res.status(400).json({ error: 'userId and content are required' });
|
||||
}
|
||||
|
||||
const post = Social.createPost({ userId, content, type, clubId, challengeId });
|
||||
res.status(201).json(post);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/social/posts/:id - Get a specific post
|
||||
router.get('/posts/:id', (req, res) => {
|
||||
try {
|
||||
const post = Social.getPost(req.params.id);
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
res.json(post);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/social/posts/:id - Delete a post
|
||||
router.delete('/posts/:id', (req, res) => {
|
||||
try {
|
||||
const deleted = Social.deletePost(req.params.id);
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/social/posts/:id/likes - Like a post
|
||||
router.post('/posts/:id/likes', (req, res) => {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'userId is required' });
|
||||
}
|
||||
|
||||
const like = Social.likePost(req.params.id, userId);
|
||||
res.status(201).json(like);
|
||||
} catch (error) {
|
||||
if (error.message === 'Already liked') {
|
||||
return res.status(409).json({ error: 'Already liked this post' });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/social/posts/:id/likes - Unlike a post
|
||||
router.delete('/posts/:id/likes', (req, res) => {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'userId is required' });
|
||||
}
|
||||
|
||||
const removed = Social.unlikePost(req.params.id, userId);
|
||||
if (!removed) {
|
||||
return res.status(404).json({ error: 'Like not found' });
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/social/posts/:id/comments - Comment on a post
|
||||
router.post('/posts/:id/comments', (req, res) => {
|
||||
try {
|
||||
const { userId, content } = req.body;
|
||||
|
||||
if (!userId || !content) {
|
||||
return res.status(400).json({ error: 'userId and content are required' });
|
||||
}
|
||||
|
||||
const comment = Social.addComment(req.params.id, { userId, content });
|
||||
res.status(201).json(comment);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/social/posts/:id/comments - Get post comments
|
||||
router.get('/posts/:id/comments', (req, res) => {
|
||||
try {
|
||||
const comments = Social.getComments(req.params.id);
|
||||
res.json(comments);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user