mayaspace-2-backend/index.js

198 lines
6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 users 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 users 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}`);
});