mayaspace-2-backend/index.js
2024-11-06 19:18:37 +00:00

191 lines
5.5 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 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
// In-memory "database" for users and posts
const users = {};
const posts = {};
const followers = {};
// 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;
if (users[username]) {
return res.status(400).json({ error: 'User already exists' });
}
// Hash the password before saving
const hashedPassword = await bcrypt.hash(password, 10);
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`,
});
users[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;
const user = users[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' });
}
if (!users[username]) {
return res.status(404).json({ error: 'User not found' });
}
const post = {
id: `http://localhost:${port}/posts/${uuid.v4()}`,
type: 'Note',
content,
author: users[username],
published: new Date().toISOString(),
};
posts[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' });
}
if (!users[username]) {
return res.status(404).json({ error: 'User not found' });
}
const userPosts = Object.values(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 (!users[username]) {
return res.status(404).json({ error: 'User not found' });
}
if (activity.type === 'Follow') {
const actor = activity.actor;
// Add follower to the users list of followers
if (!followers[username]) {
followers[username] = [];
}
followers[username].push(actor.id);
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;
if (!users[username]) {
return res.status(404).json({ error: 'User not found' });
}
res.json(users[username]);
});
// Outbox endpoint to send posts
activityPubRouter.get('/users/:username/outbox', (req, res) => {
const { username } = req.params;
if (!users[username]) {
return res.status(404).json({ error: 'User not found' });
}
// Fetch posts from this users outbox
const userPosts = Object.values(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 authentication running at http://localhost:${port}`);
});