mirror of
https://github.com/philipredstone/relnet.git
synced 2025-07-08 22:56:42 +02:00
initial commit
This commit is contained in:
42
src/app.ts
Normal file
42
src/app.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import express, { Application } from 'express';
|
||||
import cors from 'cors';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import dotenv from 'dotenv';
|
||||
import authRoutes from './routes/auth.routes';
|
||||
import networkRoutes from './routes/network.routes';
|
||||
import peopleRoutes from './routes/people.routes';
|
||||
import relationshipRoutes from './routes/relationship.routes';
|
||||
import path from "node:path";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app: Application = express();
|
||||
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use(cors({
|
||||
origin: process.env.CLIENT_URL || 'http://localhost:3000',
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/networks', networkRoutes);
|
||||
app.use('/api/networks', peopleRoutes);
|
||||
app.use('/api/networks', relationshipRoutes);
|
||||
|
||||
// Base route
|
||||
/*app.get('/', (req, res) => {
|
||||
res.send('Friendship Network API is running');
|
||||
});*/
|
||||
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../frontend/dist/')));
|
||||
|
||||
app.use((req,res,next) => {
|
||||
res.sendFile(path.join(__dirname, '..', 'frontend/dist/index.html'));
|
||||
})
|
||||
|
||||
export default app;
|
18
src/config/db.ts
Normal file
18
src/config/db.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/friendship-network';
|
||||
|
||||
const connectDB = async (): Promise<void> => {
|
||||
try {
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log('MongoDB connected successfully');
|
||||
} catch (error) {
|
||||
console.error('MongoDB connection error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export default connectDB;
|
166
src/controllers/auth.controller.ts
Normal file
166
src/controllers/auth.controller.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import User, { IUser } from '../models/user.model';
|
||||
import { UserRequest } from '../types/express';
|
||||
import { validationResult } from 'express-validator';
|
||||
|
||||
// JWT secret from environment variables
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key_change_this';
|
||||
// Token expiration (1 day)
|
||||
const TOKEN_EXPIRY = '1d';
|
||||
|
||||
// Generate JWT token
|
||||
const generateToken = (user: IUser): string => {
|
||||
return jwt.sign({ id: user._id }, JWT_SECRET, {
|
||||
expiresIn: TOKEN_EXPIRY,
|
||||
});
|
||||
};
|
||||
|
||||
// Set cookie with JWT token
|
||||
const setTokenCookie = (res: Response, token: string): void => {
|
||||
// Cookie options
|
||||
const options = {
|
||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000), // 1 day
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
};
|
||||
|
||||
res.cookie('token', token, options);
|
||||
};
|
||||
|
||||
// Register a new user
|
||||
export const register = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
if(!process.env.ENABLE_REGISTRATION)
|
||||
{
|
||||
res.status(403).json({errors: ["Registration is disabled"]});
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, password, username } = req.body;
|
||||
|
||||
// Check if user already exists
|
||||
let user = await User.findOne({ email });
|
||||
if (user) {
|
||||
res.status(400).json({ message: 'User already exists' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new user
|
||||
user = new User({
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
});
|
||||
|
||||
// Save user to database
|
||||
await user.save();
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken(user);
|
||||
|
||||
// Set token cookie
|
||||
setTokenCookie(res, token);
|
||||
|
||||
// Send response
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Login user
|
||||
export const login = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Check if user exists
|
||||
const user = await User.findOne({ email });
|
||||
if (!user) {
|
||||
res.status(400).json({ message: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if password is correct
|
||||
const isMatch = await user.comparePassword(password);
|
||||
if (!isMatch) {
|
||||
res.status(400).json({ message: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken(user);
|
||||
|
||||
// Set token cookie
|
||||
setTokenCookie(res, token);
|
||||
|
||||
// Send response
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Logout user
|
||||
export const logout = (req: Request, res: Response): void => {
|
||||
res.cookie('token', 'none', {
|
||||
expires: new Date(Date.now() + 10 * 1000), // 10 seconds
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
res.json({ success: true, message: 'Logged out successfully' });
|
||||
};
|
||||
|
||||
// Get current user
|
||||
export const getCurrentUser = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const user = req.user;
|
||||
|
||||
if (!user) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get current user error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
161
src/controllers/network.controller.ts
Normal file
161
src/controllers/network.controller.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { Response } from 'express';
|
||||
import Network from '../models/network.model';
|
||||
import { UserRequest } from '../types/express';
|
||||
import { validationResult } from 'express-validator';
|
||||
|
||||
// Get all networks for current user
|
||||
export const getUserNetworks = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const networks = await Network.find({ owner: req.user._id });
|
||||
|
||||
res.json({ success: true, data: networks });
|
||||
} catch (error) {
|
||||
console.error('Get networks error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new network
|
||||
export const createNetwork = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, description, isPublic } = req.body;
|
||||
|
||||
const network = new Network({
|
||||
name,
|
||||
description,
|
||||
owner: req.user._id,
|
||||
isPublic: isPublic || false,
|
||||
});
|
||||
|
||||
await network.save();
|
||||
|
||||
res.status(201).json({ success: true, data: network });
|
||||
} catch (error) {
|
||||
console.error('Create network error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Get a specific network
|
||||
export const getNetwork = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const networkId = req.params.id;
|
||||
|
||||
if (!req.user) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const network = await Network.findById(networkId);
|
||||
|
||||
if (!network) {
|
||||
res.status(404).json({ message: 'Network not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is owner or network is public
|
||||
if (network.owner.toString() !== req.user._id.toString() && !network.isPublic) {
|
||||
res.status(403).json({ message: 'You do not have permission to access this network' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, data: network });
|
||||
} catch (error) {
|
||||
console.error('Get network error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Update a network
|
||||
export const updateNetwork = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const networkId = req.params.id;
|
||||
|
||||
if (!req.user) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const network = await Network.findById(networkId);
|
||||
|
||||
if (!network) {
|
||||
res.status(404).json({ message: 'Network not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is owner
|
||||
if (network.owner.toString() !== req.user._id.toString()) {
|
||||
res.status(403).json({ message: 'You do not have permission to update this network' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, description, isPublic } = req.body;
|
||||
|
||||
network.name = name || network.name;
|
||||
network.description = description !== undefined ? description : network.description;
|
||||
network.isPublic = isPublic !== undefined ? isPublic : network.isPublic;
|
||||
|
||||
await network.save();
|
||||
|
||||
res.json({ success: true, data: network });
|
||||
} catch (error) {
|
||||
console.error('Update network error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a network
|
||||
export const deleteNetwork = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const networkId = req.params.id;
|
||||
|
||||
if (!req.user) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const network = await Network.findById(networkId);
|
||||
|
||||
if (!network) {
|
||||
res.status(404).json({ message: 'Network not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is owner
|
||||
if (network.owner.toString() !== req.user._id.toString()) {
|
||||
res.status(403).json({ message: 'You do not have permission to delete this network' });
|
||||
return;
|
||||
}
|
||||
|
||||
await network.deleteOne(); // Changed from remove() to deleteOne()
|
||||
|
||||
res.json({ success: true, message: 'Network deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete network error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
174
src/controllers/people.controller.ts
Normal file
174
src/controllers/people.controller.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import { Response } from 'express';
|
||||
import Person from '../models/person.model';
|
||||
import Relationship from '../models/relationship.model';
|
||||
import { UserRequest } from '../types/express';
|
||||
import { validationResult } from 'express-validator';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
// Get all people in a network
|
||||
export const getPeople = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const networkId = req.params.networkId;
|
||||
|
||||
if (!req.user || !req.network) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const people = await Person.find({ network: networkId });
|
||||
|
||||
res.json({ success: true, data: people });
|
||||
} catch (error) {
|
||||
console.error('Get people error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Add a person to the network
|
||||
export const addPerson = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const networkId = req.params.networkId;
|
||||
|
||||
if (!req.user || !req.network) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is the owner (only owners can add people)
|
||||
if (req.network.owner.toString() !== req.user._id.toString()) {
|
||||
res.status(403).json({ message: 'Only the network owner can add people' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { firstName, lastName, birthday, position } = req.body;
|
||||
|
||||
// Check if person already exists in this network
|
||||
const existingPerson = await Person.findOne({
|
||||
firstName,
|
||||
lastName,
|
||||
network: networkId,
|
||||
});
|
||||
|
||||
if (existingPerson) {
|
||||
res.status(400).json({ message: 'This person already exists in the network' });
|
||||
return;
|
||||
}
|
||||
|
||||
const person = new Person({
|
||||
firstName,
|
||||
lastName,
|
||||
birthday: birthday || undefined,
|
||||
network: networkId,
|
||||
position: position || { x: 100 + Math.random() * 500, y: 100 + Math.random() * 400 },
|
||||
});
|
||||
|
||||
await person.save();
|
||||
|
||||
res.status(201).json({ success: true, data: person });
|
||||
} catch (error) {
|
||||
console.error('Add person error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Update a person
|
||||
export const updatePerson = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const networkId = req.params.networkId;
|
||||
const personId = req.params.id;
|
||||
|
||||
if (!req.user || !req.network) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is the owner (only owners can update people)
|
||||
if (req.network.owner.toString() !== req.user._id.toString()) {
|
||||
res.status(403).json({ message: 'Only the network owner can update people' });
|
||||
return;
|
||||
}
|
||||
|
||||
const person = await Person.findOne({
|
||||
_id: personId,
|
||||
network: networkId,
|
||||
});
|
||||
|
||||
if (!person) {
|
||||
res.status(404).json({ message: 'Person not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { firstName, lastName, birthday, position } = req.body;
|
||||
|
||||
// Update person
|
||||
if (firstName) person.firstName = firstName;
|
||||
if (lastName) person.lastName = lastName;
|
||||
if (birthday !== undefined) person.birthday = birthday || undefined;
|
||||
if (position) person.position = position;
|
||||
|
||||
await person.save();
|
||||
|
||||
res.json({ success: true, data: person });
|
||||
} catch (error) {
|
||||
console.error('Update person error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Remove a person from the network
|
||||
export const removePerson = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const networkId = req.params.networkId;
|
||||
const personId = req.params.id;
|
||||
|
||||
if (!req.user || !req.network) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is the owner (only owners can remove people)
|
||||
if (req.network.owner.toString() !== req.user._id.toString()) {
|
||||
res.status(403).json({ message: 'Only the network owner can remove people' });
|
||||
return;
|
||||
}
|
||||
|
||||
const person = await Person.findOne({
|
||||
_id: personId,
|
||||
network: networkId,
|
||||
});
|
||||
|
||||
if (!person) {
|
||||
res.status(404).json({ message: 'Person not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all relationships involving this person
|
||||
await Relationship.deleteMany({
|
||||
network: networkId,
|
||||
$or: [{ source: personId }, { target: personId }],
|
||||
});
|
||||
|
||||
// Remove the person
|
||||
await person.deleteOne(); // Changed from remove() to deleteOne()
|
||||
|
||||
res.json({ success: true, message: 'Person and associated relationships removed successfully' });
|
||||
} catch (error) {
|
||||
console.error('Remove person error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
180
src/controllers/relationship.controller.ts
Normal file
180
src/controllers/relationship.controller.ts
Normal file
@ -0,0 +1,180 @@
|
||||
import { Response } from 'express';
|
||||
import Relationship from '../models/relationship.model';
|
||||
import Person from '../models/person.model';
|
||||
import { UserRequest } from '../types/express';
|
||||
import { validationResult } from 'express-validator';
|
||||
|
||||
// Get all relationships in a network
|
||||
export const getRelationships = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const networkId = req.params.networkId;
|
||||
|
||||
if (!req.user || !req.network) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const relationships = await Relationship.find({ network: networkId });
|
||||
|
||||
res.json({ success: true, data: relationships });
|
||||
} catch (error) {
|
||||
console.error('Get relationships error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Add a relationship to the network
|
||||
export const addRelationship = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const networkId = req.params.networkId;
|
||||
|
||||
if (!req.user || !req.network) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is the owner (only owners can add relationships)
|
||||
if (req.network.owner.toString() !== req.user._id.toString()) {
|
||||
res.status(403).json({ message: 'Only the network owner can add relationships' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { source, target, type, customType } = req.body;
|
||||
|
||||
// Check if source and target exist and belong to the network
|
||||
const sourcePerson = await Person.findOne({
|
||||
_id: source,
|
||||
network: networkId,
|
||||
});
|
||||
|
||||
const targetPerson = await Person.findOne({
|
||||
_id: target,
|
||||
network: networkId,
|
||||
});
|
||||
|
||||
if (!sourcePerson || !targetPerson) {
|
||||
res.status(400).json({ message: 'Source or target person not found in this network' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if relationship already exists
|
||||
const existingRelationship = await Relationship.findOne({
|
||||
$or: [
|
||||
{ source, target, network: networkId },
|
||||
{ source: target, target: source, network: networkId },
|
||||
],
|
||||
});
|
||||
|
||||
if (existingRelationship) {
|
||||
res.status(400).json({ message: 'A relationship already exists between these people' });
|
||||
return;
|
||||
}
|
||||
|
||||
const relationship = new Relationship({
|
||||
source,
|
||||
target,
|
||||
type,
|
||||
customType: type === 'custom' ? customType : undefined,
|
||||
network: networkId,
|
||||
});
|
||||
|
||||
await relationship.save();
|
||||
|
||||
res.status(201).json({ success: true, data: relationship });
|
||||
} catch (error) {
|
||||
console.error('Add relationship error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Update a relationship
|
||||
export const updateRelationship = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const networkId = req.params.networkId;
|
||||
const relationshipId = req.params.id;
|
||||
|
||||
if (!req.user || !req.network) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is the owner (only owners can update relationships)
|
||||
if (req.network.owner.toString() !== req.user._id.toString()) {
|
||||
res.status(403).json({ message: 'Only the network owner can update relationships' });
|
||||
return;
|
||||
}
|
||||
|
||||
const relationship = await Relationship.findOne({
|
||||
_id: relationshipId,
|
||||
network: networkId,
|
||||
});
|
||||
|
||||
if (!relationship) {
|
||||
res.status(404).json({ message: 'Relationship not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, customType } = req.body;
|
||||
|
||||
// Update relationship
|
||||
if (type) relationship.type = type;
|
||||
if (type === 'custom' && customType) relationship.customType = customType;
|
||||
|
||||
await relationship.save();
|
||||
|
||||
res.json({ success: true, data: relationship });
|
||||
} catch (error) {
|
||||
console.error('Update relationship error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Remove a relationship
|
||||
export const removeRelationship = async (req: UserRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const networkId = req.params.networkId;
|
||||
const relationshipId = req.params.id;
|
||||
|
||||
if (!req.user || !req.network) {
|
||||
res.status(401).json({ message: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is the owner (only owners can remove relationships)
|
||||
if (req.network.owner.toString() !== req.user._id.toString()) {
|
||||
res.status(403).json({ message: 'Only the network owner can remove relationships' });
|
||||
return;
|
||||
}
|
||||
|
||||
const relationship = await Relationship.findOne({
|
||||
_id: relationshipId,
|
||||
network: networkId,
|
||||
});
|
||||
|
||||
if (!relationship) {
|
||||
res.status(404).json({ message: 'Relationship not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
await relationship.deleteOne(); // Changed from remove() to deleteOne()
|
||||
|
||||
res.json({ success: true, message: 'Relationship removed successfully' });
|
||||
} catch (error) {
|
||||
console.error('Remove relationship error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
39
src/middleware/auth.middleware.ts
Normal file
39
src/middleware/auth.middleware.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import User from '../models/user.model';
|
||||
import { UserRequest } from '../types/express';
|
||||
|
||||
// JWT secret from environment variables
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key_change_this';
|
||||
|
||||
export const auth = async (req: UserRequest, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
// Get token from cookie or authorization header
|
||||
const token = req.cookies.token ||
|
||||
(req.headers.authorization && req.headers.authorization.startsWith('Bearer')
|
||||
? req.headers.authorization.split(' ')[1]
|
||||
: null);
|
||||
|
||||
if (!token) {
|
||||
res.status(401).json({ message: 'No token, authorization denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify token
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
||||
|
||||
// Find user by id
|
||||
const user = await User.findById(decoded.id).select('-password');
|
||||
|
||||
if (!user) {
|
||||
res.status(401).json({ message: 'User not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Set user in request object
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ message: 'Token is not valid' });
|
||||
}
|
||||
};
|
33
src/middleware/network-access.middleware.ts
Normal file
33
src/middleware/network-access.middleware.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Response, NextFunction } from 'express';
|
||||
import Network from '../models/network.model';
|
||||
import { UserRequest } from '../types/express';
|
||||
|
||||
export const checkNetworkAccess = async (req: UserRequest, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
const networkId = req.params.networkId;
|
||||
|
||||
if (!networkId) {
|
||||
res.status(400).json({ message: 'Network ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const network = await Network.findById(networkId);
|
||||
|
||||
if (!network) {
|
||||
res.status(404).json({ message: 'Network not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is the owner or the network is public
|
||||
if (network.owner.toString() !== req.user?._id.toString() && !network.isPublic) {
|
||||
res.status(403).json({ message: 'You do not have permission to access this network' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Add network to the request
|
||||
req.network = network;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
36
src/models/network.model.ts
Normal file
36
src/models/network.model.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
|
||||
export interface INetwork extends Document {
|
||||
_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
owner: mongoose.Types.ObjectId;
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
const NetworkSchema = new Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: [true, 'Network name is required'],
|
||||
trim: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
owner: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
isPublic: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
export default mongoose.model<INetwork>('Network', NetworkSchema);
|
||||
|
52
src/models/person.model.ts
Normal file
52
src/models/person.model.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
|
||||
export interface IPerson extends Document {
|
||||
_id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthday?: Date;
|
||||
network: mongoose.Types.ObjectId;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
|
||||
const PersonSchema = new Schema(
|
||||
{
|
||||
firstName: {
|
||||
type: String,
|
||||
required: [true, 'First name is required'],
|
||||
trim: true,
|
||||
},
|
||||
lastName: {
|
||||
type: String,
|
||||
required: [true, 'Last name is required'],
|
||||
trim: true,
|
||||
},
|
||||
birthday: {
|
||||
type: Date,
|
||||
},
|
||||
network: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Network',
|
||||
required: true,
|
||||
},
|
||||
position: {
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
// Create compound index to ensure uniqueness within a network
|
||||
PersonSchema.index({ firstName: 1, lastName: 1, network: 1 }, { unique: true });
|
||||
|
||||
export default mongoose.model<IPerson>('Person', PersonSchema);
|
48
src/models/relationship.model.ts
Normal file
48
src/models/relationship.model.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
|
||||
export interface IRelationship extends Document {
|
||||
_id: string;
|
||||
source: mongoose.Types.ObjectId;
|
||||
target: mongoose.Types.ObjectId;
|
||||
type: string;
|
||||
customType?: string;
|
||||
network: mongoose.Types.ObjectId;
|
||||
}
|
||||
|
||||
const RelationshipSchema = new Schema(
|
||||
{
|
||||
source: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Person',
|
||||
required: true,
|
||||
},
|
||||
target: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Person',
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: [true, 'Relationship type is required'],
|
||||
enum: ['freund', 'partner', 'familie', 'arbeitskolleg', 'custom'],
|
||||
},
|
||||
customType: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
network: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Network',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
// Create compound index to ensure unique relationships in a network
|
||||
RelationshipSchema.index(
|
||||
{ source: 1, target: 1, network: 1 },
|
||||
{ unique: true }
|
||||
);
|
||||
|
||||
export default mongoose.model<IRelationship>('Relationship', RelationshipSchema);
|
54
src/models/user.model.ts
Normal file
54
src/models/user.model.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export interface IUser extends Document {
|
||||
_id: string;
|
||||
email: string;
|
||||
password: string;
|
||||
username: string;
|
||||
comparePassword(candidatePassword: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
const UserSchema = new Schema(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: [true, 'Email is required'],
|
||||
unique: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: [true, 'Password is required'],
|
||||
minlength: 6,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: [true, 'Username is required'],
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
// Hash password before saving
|
||||
UserSchema.pre('save', async function(next) {
|
||||
const user = this;
|
||||
if (!user.isModified('password')) return next();
|
||||
|
||||
try {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
user.password = await bcrypt.hash(user.password, salt);
|
||||
next();
|
||||
} catch (error: any) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Compare password method
|
||||
UserSchema.methods.comparePassword = async function(candidatePassword: string): Promise<boolean> {
|
||||
return bcrypt.compare(candidatePassword, this.password);
|
||||
};
|
||||
|
||||
export default mongoose.model<IUser>('User', UserSchema);
|
43
src/routes/auth.routes.ts
Normal file
43
src/routes/auth.routes.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import express from 'express';
|
||||
import { check } from 'express-validator';
|
||||
import * as authController from '../controllers/auth.controller';
|
||||
import { auth } from '../middleware/auth.middleware';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// @route POST /api/auth/register
|
||||
// @desc Register a new user
|
||||
// @access Public
|
||||
router.post(
|
||||
'/register',
|
||||
[
|
||||
check('email', 'Please include a valid email').isEmail(),
|
||||
check('password', 'Password must be at least 6 characters').isLength({ min: 6 }),
|
||||
check('username', 'Username is required').not().isEmpty(),
|
||||
],
|
||||
authController.register
|
||||
);
|
||||
|
||||
// @route POST /api/auth/login
|
||||
// @desc Login user
|
||||
// @access Public
|
||||
router.post(
|
||||
'/login',
|
||||
[
|
||||
check('email', 'Please include a valid email').isEmail(),
|
||||
check('password', 'Password is required').exists(),
|
||||
],
|
||||
authController.login
|
||||
);
|
||||
|
||||
// @route POST /api/auth/logout
|
||||
// @desc Logout user
|
||||
// @access Private
|
||||
router.post('/logout', authController.logout);
|
||||
|
||||
// @route GET /api/auth/me
|
||||
// @desc Get current user
|
||||
// @access Private
|
||||
router.get('/me', auth, authController.getCurrentUser);
|
||||
|
||||
export default router;
|
48
src/routes/network.routes.ts
Normal file
48
src/routes/network.routes.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import express from 'express';
|
||||
import { check } from 'express-validator';
|
||||
import * as networkController from '../controllers/network.controller';
|
||||
import { auth } from '../middleware/auth.middleware';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.use(auth);
|
||||
|
||||
// @route GET /api/networks
|
||||
// @desc Get all networks for current user
|
||||
// @access Private
|
||||
router.get('/', networkController.getUserNetworks);
|
||||
|
||||
// @route POST /api/networks
|
||||
// @desc Create a new network
|
||||
// @access Private
|
||||
router.post(
|
||||
'/',
|
||||
[
|
||||
check('name', 'Network name is required').not().isEmpty(),
|
||||
],
|
||||
networkController.createNetwork
|
||||
);
|
||||
|
||||
// @route GET /api/networks/:id
|
||||
// @desc Get a specific network
|
||||
// @access Private
|
||||
router.get('/:id', networkController.getNetwork);
|
||||
|
||||
// @route PUT /api/networks/:id
|
||||
// @desc Update a network
|
||||
// @access Private
|
||||
router.put(
|
||||
'/:id',
|
||||
[
|
||||
check('name', 'Network name is required if provided').optional().not().isEmpty(),
|
||||
],
|
||||
networkController.updateNetwork
|
||||
);
|
||||
|
||||
// @route DELETE /api/networks/:id
|
||||
// @desc Delete a network
|
||||
// @access Private
|
||||
router.delete('/:id', networkController.deleteNetwork);
|
||||
|
||||
export default router;
|
48
src/routes/people.routes.ts
Normal file
48
src/routes/people.routes.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import express from 'express';
|
||||
import { check } from 'express-validator';
|
||||
import * as peopleController from '../controllers/people.controller';
|
||||
import { auth } from '../middleware/auth.middleware';
|
||||
import { checkNetworkAccess } from '../middleware/network-access.middleware';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require authentication and network access check
|
||||
router.use('/:networkId/people', auth, checkNetworkAccess);
|
||||
|
||||
// @route GET /api/networks/:networkId/people
|
||||
// @desc Get all people in a network
|
||||
// @access Private
|
||||
router.get('/:networkId/people', peopleController.getPeople);
|
||||
|
||||
// @route POST /api/networks/:networkId/people
|
||||
// @desc Add a person to the network
|
||||
// @access Private
|
||||
router.post(
|
||||
'/:networkId/people',
|
||||
[
|
||||
check('firstName', 'First name is required').not().isEmpty(),
|
||||
check('lastName', 'Last name is required').not().isEmpty(),
|
||||
check('birthday', 'Birthday must be a valid date if provided').optional().isISO8601().toDate(),
|
||||
],
|
||||
peopleController.addPerson
|
||||
);
|
||||
|
||||
// @route PUT /api/networks/:networkId/people/:id
|
||||
// @desc Update a person
|
||||
// @access Private
|
||||
router.put(
|
||||
'/:networkId/people/:id',
|
||||
[
|
||||
check('firstName', 'First name must not be empty if provided').optional().not().isEmpty(),
|
||||
check('lastName', 'Last name must not be empty if provided').optional().not().isEmpty(),
|
||||
check('birthday', 'Birthday must be a valid date if provided').optional().isISO8601().toDate(),
|
||||
],
|
||||
peopleController.updatePerson
|
||||
);
|
||||
|
||||
// @route DELETE /api/networks/:networkId/people/:id
|
||||
// @desc Remove a person from the network
|
||||
// @access Private
|
||||
router.delete('/:networkId/people/:id', peopleController.removePerson);
|
||||
|
||||
export default router;
|
56
src/routes/relationship.routes.ts
Normal file
56
src/routes/relationship.routes.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import express from 'express';
|
||||
import { check } from 'express-validator';
|
||||
import * as relationshipController from '../controllers/relationship.controller';
|
||||
import { auth } from '../middleware/auth.middleware';
|
||||
import { checkNetworkAccess } from '../middleware/network-access.middleware';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require authentication and network access check
|
||||
router.use('/:networkId/relationships', auth, checkNetworkAccess);
|
||||
|
||||
// @route GET /api/networks/:networkId/relationships
|
||||
// @desc Get all relationships in a network
|
||||
// @access Private
|
||||
router.get('/:networkId/relationships', relationshipController.getRelationships);
|
||||
|
||||
// @route POST /api/networks/:networkId/relationships
|
||||
// @desc Add a relationship to the network
|
||||
// @access Private
|
||||
router.post(
|
||||
'/:networkId/relationships',
|
||||
[
|
||||
check('source', 'Source person ID is required').not().isEmpty().isMongoId(),
|
||||
check('target', 'Target person ID is required').not().isEmpty().isMongoId(),
|
||||
check('type', 'Relationship type is required').isIn(['freund', 'partner', 'familie', 'arbeitskolleg', 'custom']),
|
||||
check('customType', 'Custom type is required when type is custom')
|
||||
.if(check('type').equals('custom'))
|
||||
.not()
|
||||
.isEmpty(),
|
||||
],
|
||||
relationshipController.addRelationship
|
||||
);
|
||||
|
||||
// @route PUT /api/networks/:networkId/relationships/:id
|
||||
// @desc Update a relationship
|
||||
// @access Private
|
||||
router.put(
|
||||
'/:networkId/relationships/:id',
|
||||
[
|
||||
check('type', 'Relationship type must be valid if provided')
|
||||
.optional()
|
||||
.isIn(['freund', 'partner', 'familie', 'arbeitskolleg', 'custom']),
|
||||
check('customType', 'Custom type is required when type is custom')
|
||||
.if(check('type').equals('custom'))
|
||||
.not()
|
||||
.isEmpty(),
|
||||
],
|
||||
relationshipController.updateRelationship
|
||||
);
|
||||
|
||||
// @route DELETE /api/networks/:networkId/relationships/:id
|
||||
// @desc Remove a relationship
|
||||
// @access Private
|
||||
router.delete('/:networkId/relationships/:id', relationshipController.removeRelationship);
|
||||
|
||||
export default router;
|
15
src/server.ts
Normal file
15
src/server.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import app from './app';
|
||||
import connectDB from './config/db';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
// Connect to MongoDB
|
||||
connectDB();
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
9
src/types/express.d.ts
vendored
Normal file
9
src/types/express.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
import { Request } from 'express';
|
||||
import { IUser } from '../models/user.model';
|
||||
import { INetwork } from '../models/network.model';
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
export interface UserRequest extends Request {
|
||||
user?: IUser;
|
||||
network?: INetwork;
|
||||
}
|
Reference in New Issue
Block a user