mayaspace-2-backend/index.js

198 lines
6 KiB
JavaScript

const express = require('express');
const bodyParser = require('body-parser');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { ActivityPubRouter, createActor } = require('activitypub-express');
const uuid = require('uuid');
const db = require('quick.db'); // Import quick.db
const fetch = require('node-fetch');
const app = express();
const port = 3000;
const SECRET_KEY = 'your_secret_key'; // Replace with a secure key in a real app
// Middleware to parse JSON
app.use(bodyParser.json());
// Helper function to create an actor URL
const actorURL = (username) => `http://localhost:${port}/users/${username}`;
// Helper function to generate JWT token
const generateToken = (user) => {
return jwt.sign({ id: user.username }, SECRET_KEY, { expiresIn: '1h' });
};
// Middleware to protect routes
const authenticate = (req, res, next) => {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = decoded;
next();
});
};
// Route to create a new user (actor with password)
app.post('/users', async (req, res) => {
const { username, displayName, password } = req.body;
// Check if user already exists
const existingUser = db.get(`user_${username}`);
if (existingUser) {
return res.status(400).json({ error: 'User already exists' });
}
// Hash the password before saving
const hashedPassword = await bcrypt.hash(password, 10);
// Create the actor and store it in the DB
const actor = createActor({
id: actorURL(username),
username,
displayName,
inbox: `http://localhost:${port}/users/${username}/inbox`,
outbox: `http://localhost:${port}/users/${username}/outbox`,
followers: `http://localhost:${port}/users/${username}/followers`,
following: `http://localhost:${port}/users/${username}/following`,
});
// Store actor info and password in the database
db.set(`user_${username}`, { ...actor, password: hashedPassword });
res.status(201).json(actor);
});
// Route to log in a user and get a token
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Retrieve user from the database
const user = db.get(`user_${username}`);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Compare hashed password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'Invalid password' });
}
// Generate and return JWT token
const token = generateToken(user);
res.json({ token });
});
// Route to create a post (status update) with authentication
app.post('/users/:username/posts', authenticate, (req, res) => {
const { username } = req.params;
const { content } = req.body;
if (req.user.id !== username) {
return res.status(403).json({ error: 'You can only post as yourself' });
}
// Retrieve the user from the database
const user = db.get(`user_${username}`);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Create a post and store it in the database
const post = {
id: `http://localhost:${port}/posts/${uuid.v4()}`,
type: 'Note',
content,
author: user,
published: new Date().toISOString(),
};
// Store the post in the database
db.set(`post_${post.id}`, post);
res.status(201).json(post);
});
// Route to get a user's posts
app.get('/users/:username/posts', authenticate, (req, res) => {
const { username } = req.params;
if (req.user.id !== username) {
return res.status(403).json({ error: 'You can only view your own posts' });
}
// Retrieve all posts from the database
const userPosts = Object.values(db.get("posts") || {}).filter(post => post.author.username === username);
res.json(userPosts);
});
// ActivityPub setup
const activityPubRouter = new ActivityPubRouter();
// Handle incoming Follow activity
activityPubRouter.post('/users/:username/inbox', authenticate, async (req, res) => {
const { username } = req.params;
const activity = req.body;
if (!db.get(`user_${username}`)) {
return res.status(404).json({ error: 'User not found' });
}
if (activity.type === 'Follow') {
const actor = activity.actor;
// Retrieve followers list and add new follower
const followersList = db.get(`followers_${username}`) || [];
followersList.push(actor.id);
// Store the updated followers list in the database
db.set(`followers_${username}`, followersList);
res.status(200).json({ status: 'Followed successfully' });
// Optionally, notify the actor that the follow was accepted
await sendFollowResponse(actor, username);
} else {
res.status(400).json({ error: 'Unsupported activity type' });
}
});
// Federated actor info endpoint
activityPubRouter.get('/users/:username', (req, res) => {
const { username } = req.params;
const user = db.get(`user_${username}`);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// Outbox endpoint to send posts
activityPubRouter.get('/users/:username/outbox', (req, res) => {
const { username } = req.params;
const user = db.get(`user_${username}`);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Fetch posts from this user's outbox (from the DB)
const userPosts = Object.values(db.get("posts") || {}).filter(post => post.author.username === username);
res.json(userPosts);
});
app.use(activityPubRouter.router);
// Start the server
app.listen(port, () => {
console.log(`Federated social network with SQLite (quick.db) running at http://localhost:${port}`);
});