Finished guestbook and gallery. started diary
This commit is contained in:
@@ -13,6 +13,16 @@ class SocialNetworkController {
|
||||
this.getFolderImageList = this.getFolderImageList.bind(this);
|
||||
this.getImageByHash = this.getImageByHash.bind(this);
|
||||
this.changeImage = this.changeImage.bind(this);
|
||||
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
|
||||
this.deleteFolder = this.deleteFolder.bind(this);
|
||||
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
|
||||
this.getGuestbookEntries = this.getGuestbookEntries.bind(this);
|
||||
this.deleteGuestbookEntry = this.deleteGuestbookEntry.bind(this);
|
||||
this.getGuestbookImage = this.getGuestbookImage.bind(this);
|
||||
this.createDiaryEntry = this.createDiaryEntry.bind(this);
|
||||
this.updateDiaryEntry = this.updateDiaryEntry.bind(this);
|
||||
this.deleteDiaryEntry = this.deleteDiaryEntry.bind(this);
|
||||
this.getDiaryEntries = this.getDiaryEntries.bind(this);
|
||||
}
|
||||
|
||||
async userSearch(req, res) {
|
||||
@@ -45,7 +55,8 @@ class SocialNetworkController {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const folderData = req.body;
|
||||
const folder = await this.socialNetworkService.createFolder(userId, folderData);
|
||||
const { folderId } = req.params;
|
||||
const folder = await this.socialNetworkService.createFolder(userId, folderData, folderId);
|
||||
res.status(201).json(folder);
|
||||
} catch (error) {
|
||||
console.error('Error in createFolder:', error);
|
||||
@@ -115,7 +126,6 @@ class SocialNetworkController {
|
||||
const userId = req.headers.userid;
|
||||
const { hash } = req.params;
|
||||
const filePath = await this.socialNetworkService.getImageFilePath(userId, hash);
|
||||
console.log(filePath);
|
||||
res.sendFile(filePath, err => {
|
||||
if (err) {
|
||||
console.error('Error sending file:', err);
|
||||
@@ -141,6 +151,141 @@ class SocialNetworkController {
|
||||
res.status(403).json({ error: error.message || 'Access denied or image not found' });
|
||||
}
|
||||
}
|
||||
|
||||
async getFoldersByUsername(req, res) {
|
||||
try {
|
||||
const { username } = req.params;
|
||||
const requestingUserId = req.headers.userid;
|
||||
if (!username || !requestingUserId) {
|
||||
return res.status(400).json({ error: 'Invalid username or requesting user ID.' });
|
||||
}
|
||||
const folders = await this.socialNetworkService.getFoldersByUsername(username, requestingUserId);
|
||||
if (!folders) {
|
||||
return res.status(404).json({ error: 'No folders found or access denied.' });
|
||||
}
|
||||
res.status(200).json(folders);
|
||||
} catch (error) {
|
||||
console.error('Error in getFoldersByUsername:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFolder(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const { folderId } = req.params;
|
||||
await this.socialNetworkService.deleteFolder(userId, folderId);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Error in deleteFolder:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async createGuestbookEntry(req, res) {
|
||||
try {
|
||||
const { htmlContent, recipientName } = req.body;
|
||||
const hashedSenderId = req.headers.userid;
|
||||
const image = req.file ? req.file : null;
|
||||
const entry = await this.socialNetworkService.createGuestbookEntry(
|
||||
hashedSenderId,
|
||||
recipientName,
|
||||
htmlContent,
|
||||
image
|
||||
);
|
||||
res.status(201).json(entry);
|
||||
} catch (error) {
|
||||
console.error('Error in createGuestbookEntry:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getGuestbookEntries(req, res) {
|
||||
try {
|
||||
const hashedUserId = req.headers.userid;
|
||||
const { username, page = 1 } = req.params;
|
||||
const entries = await this.socialNetworkService.getGuestbookEntries(hashedUserId, username, page);
|
||||
res.status(200).json(entries);
|
||||
} catch (error) {
|
||||
console.error('Error in getGuestbookEntries:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteGuestbookEntry(req, res) {
|
||||
try {
|
||||
const hashedUserId = req.headers.userid;
|
||||
const { entryId } = req.params;
|
||||
await this.socialNetworkService.deleteGuestbookEntry(hashedUserId, entryId);
|
||||
res.status(200).json({ message: 'Entry deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error in deleteGuestbookEntry:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getGuestbookImage(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const { guestbookUserName, entryId } = req.params;
|
||||
const filePath = await this.socialNetworkService.getGuestbookImageFilePath(userId, guestbookUserName, entryId);
|
||||
res.sendFile(filePath, err => {
|
||||
if (err) {
|
||||
console.error('Error sending file:', err);
|
||||
res.status(500).json({ error: 'Error sending file' });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in getImageByHash:', error);
|
||||
res.status(403).json({ error: error.message || 'Access denied or image not found' });
|
||||
}
|
||||
}
|
||||
|
||||
async createDiaryEntry(req, res) {
|
||||
try {
|
||||
const { userId, text } = req.body;
|
||||
const entry = await this.socialNetworkService.createDiaryEntry(userId, text);
|
||||
res.status(201).json(entry);
|
||||
} catch (error) {
|
||||
console.error('Error in createDiaryEntry:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async updateDiaryEntry(req, res) {
|
||||
try {
|
||||
const { diaryId } = req.params;
|
||||
const { userId, text } = req.body;
|
||||
const updatedEntry = await this.socialNetworkService.updateDiaryEntry(diaryId, userId, text);
|
||||
res.status(200).json(updatedEntry);
|
||||
} catch (error) {
|
||||
console.error('Error in updateDiaryEntry:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDiaryEntry(req, res) {
|
||||
try {
|
||||
const { diaryId } = req.params;
|
||||
const { userId } = req.body;
|
||||
const result = await this.socialNetworkService.deleteDiaryEntry(diaryId, userId);
|
||||
res.status(200).json({ message: 'Entry deleted successfully', result });
|
||||
} catch (error) {
|
||||
console.error('Error in deleteDiaryEntry:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getDiaryEntries(req, res) {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const entries = await this.socialNetworkService.getDiaryEntries(userId);
|
||||
res.status(200).json(entries);
|
||||
} catch (error) {
|
||||
console.error('Error in getDiaryEntries:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SocialNetworkController;
|
||||
|
||||
@@ -17,6 +17,7 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
|
||||
import FolderImageVisibility from './community/folder_image_visibility.js';
|
||||
import ImageImageVisibility from './community/image_image_visibility.js';
|
||||
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
||||
import GuestbookEntry from './community/guestbook.js';
|
||||
|
||||
export default function setupAssociations() {
|
||||
SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' });
|
||||
@@ -95,4 +96,25 @@ export default function setupAssociations() {
|
||||
foreignKey: 'visibilityUserId',
|
||||
otherKey: 'folderId'
|
||||
});
|
||||
|
||||
User.hasMany(GuestbookEntry, {
|
||||
foreignKey: 'recipientId',
|
||||
as: 'receivedEntries'
|
||||
});
|
||||
|
||||
User.hasMany(GuestbookEntry, {
|
||||
foreignKey: 'senderId',
|
||||
as: 'sentEntries'
|
||||
});
|
||||
|
||||
GuestbookEntry.belongsTo(User, {
|
||||
foreignKey: 'recipientId',
|
||||
as: 'recipient'
|
||||
});
|
||||
|
||||
GuestbookEntry.belongsTo(User, {
|
||||
foreignKey: 'senderId',
|
||||
as: 'sender'
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
31
backend/models/community/diary.js
Normal file
31
backend/models/community/diary.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class Diary extends Model { }
|
||||
|
||||
Diary.init({
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'Diary',
|
||||
tableName: 'diary',
|
||||
schema: 'community',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default Diary;
|
||||
35
backend/models/community/diary_history.js
Normal file
35
backend/models/community/diary_history.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class DiaryHistory extends Model { }
|
||||
|
||||
DiaryHistory.init({
|
||||
diaryId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
oldText: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
oldCreatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
oldUpdatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'DiaryHistory',
|
||||
tableName: 'diary_history',
|
||||
schema: 'community',
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
export default DiaryHistory;
|
||||
47
backend/models/community/guestbook.js
Normal file
47
backend/models/community/guestbook.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
import User from './user.js';
|
||||
|
||||
const GuestbookEntry = sequelize.define('guestbook_entry', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
},
|
||||
recipientId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: User,
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
senderId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: User,
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
senderUsername: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
contentHtml: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
imageUrl: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
tableName: 'guestbook_entry',
|
||||
schema: 'community',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default GuestbookEntry;
|
||||
@@ -19,6 +19,9 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
|
||||
import FolderImageVisibility from './community/folder_image_visibility.js';
|
||||
import ImageImageVisibility from './community/image_image_visibility.js';
|
||||
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
||||
import GuestbookEntry from './community/guestbook.js';
|
||||
import DiaryHistory from './community/diary_history.js';
|
||||
import Diary from './community/diary.js';
|
||||
|
||||
const models = {
|
||||
SettingsType,
|
||||
@@ -42,6 +45,9 @@ const models = {
|
||||
FolderImageVisibility,
|
||||
ImageImageVisibility,
|
||||
FolderVisibilityUser,
|
||||
GuestbookEntry,
|
||||
DiaryHistory,
|
||||
Diary,
|
||||
};
|
||||
|
||||
export default models;
|
||||
|
||||
@@ -22,23 +22,43 @@ export async function createTriggers() {
|
||||
`;
|
||||
|
||||
const createInsertTrigger = `
|
||||
CREATE TRIGGER trigger_create_user_param_visibility
|
||||
CREATE OR REPLACE TRIGGER trigger_create_user_param_visibility
|
||||
AFTER INSERT ON community.user_param
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_user_param_visibility_trigger();
|
||||
`;
|
||||
|
||||
const createUpdateTrigger = `
|
||||
CREATE TRIGGER trigger_update_user_param_visibility
|
||||
CREATE OR REPLACE TRIGGER trigger_update_user_param_visibility
|
||||
AFTER UPDATE ON community.user_param
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_user_param_visibility_trigger();
|
||||
`;
|
||||
|
||||
const createDiaryHistoryTriggerFunction = `
|
||||
CREATE OR REPLACE FUNCTION insert_diary_history()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO community.diary_history (diaryId, userId, oldText, oldCreatedAt, oldUpdatedAt)
|
||||
VALUES (OLD.id, OLD.userId, OLD.text, OLD.createdAt, OLD.updatedAt);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`;
|
||||
|
||||
const createDiaryHistoryTrigger = `
|
||||
CREATE OR REPLACE TRIGGER diary_update_trigger
|
||||
BEFORE UPDATE ON community.diary
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION insert_diary_history();
|
||||
`;
|
||||
|
||||
try {
|
||||
await sequelize.query(createTriggerFunction);
|
||||
await sequelize.query(createInsertTrigger);
|
||||
await sequelize.query(createUpdateTrigger);
|
||||
await sequelize.query(createDiaryHistoryTriggerFunction);
|
||||
await sequelize.query(createDiaryHistoryTrigger);
|
||||
|
||||
console.log('Triggers created successfully');
|
||||
} catch (error) {
|
||||
|
||||
826
backend/package-lock.json
generated
826
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,16 +14,19 @@
|
||||
"amqplib": "^0.10.4",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dompurify": "^3.1.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"i18n": "^0.15.1",
|
||||
"joi": "^17.13.3",
|
||||
"jsdom": "^25.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.10.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"pg": "^8.12.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io": "^4.7.5",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
|
||||
@@ -8,8 +8,8 @@ const router = express.Router();
|
||||
const socialNetworkController = new SocialNetworkController();
|
||||
|
||||
router.post('/usersearch', authenticate, socialNetworkController.userSearch);
|
||||
router.get('/profile/:userId', authenticate, socialNetworkController.profile);
|
||||
router.post('/folders', authenticate, socialNetworkController.createFolder);
|
||||
router.get('/profile/main/:userId', authenticate, socialNetworkController.profile);
|
||||
router.post('/folders/:folderId', authenticate, socialNetworkController.createFolder);
|
||||
router.get('/folders', authenticate, socialNetworkController.getFolders);
|
||||
router.get('/folder/:folderId', authenticate, socialNetworkController.getFolderImageList);
|
||||
router.post('/images', authenticate, upload.single('image'), socialNetworkController.uploadImage);
|
||||
@@ -17,5 +17,15 @@ router.get('/images/:imageId', authenticate, socialNetworkController.getImage);
|
||||
router.put('/images/:imageId', authenticate, socialNetworkController.changeImage);
|
||||
router.get('/imagevisibilities', authenticate, socialNetworkController.getImageVisibilityTypes);
|
||||
router.get('/image/:hash', authenticate, socialNetworkController.getImageByHash);
|
||||
router.get('/profile/images/folders/:username', authenticate, socialNetworkController.getFoldersByUsername);
|
||||
router.delete('/folders/:folderId', authenticate, socialNetworkController.deleteFolder);
|
||||
router.post('/guestbook/entries', authenticate, upload.single('image'), socialNetworkController.createGuestbookEntry);
|
||||
router.get('/guestbook/entries/:username/:page', authenticate, socialNetworkController.getGuestbookEntries);
|
||||
router.delete('/guestbook/entries/:entryId', authenticate, socialNetworkController.deleteGuestbookEntry);
|
||||
router.get('/guestbook/image/:guestbookUserName/:entryId', authenticate, socialNetworkController.getGuestbookImage);
|
||||
router.post('/diary', authenticate, socialNetworkController.createDiaryEntry);
|
||||
router.put('/diary/:diaryId', authenticate, socialNetworkController.updateDiaryEntry);
|
||||
router.delete('/diary/:diaryId', authenticate, socialNetworkController.deleteDiaryEntry);
|
||||
router.get('/diary/:userId', authenticate, socialNetworkController.getDiaryEntries);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -51,6 +51,18 @@ class BaseService {
|
||||
const ageDate = new Date(ageDifMs);
|
||||
return Math.abs(ageDate.getUTCFullYear() - 1970);
|
||||
}
|
||||
|
||||
|
||||
async isUserAdult(userId) {
|
||||
const birthdateParam = await this.getUserParams(userId, ['birthdate']);
|
||||
if (!birthdateParam || birthdateParam.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const birthdate = birthdateParam[0].value;
|
||||
const age = this.calculateAge(birthdate);
|
||||
return age >= 18;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BaseService;
|
||||
|
||||
@@ -11,10 +11,17 @@ import Image from '../models/community/image.js';
|
||||
import ImageVisibilityType from '../models/type/image_visibility.js';
|
||||
import FolderImageVisibility from '../models/community/folder_image_visibility.js';
|
||||
import ImageImageVisibility from '../models/community/image_image_visibility.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs';
|
||||
import fsPromises from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import FolderVisibilityUser from '../models/community/folder_visibility_user.js';
|
||||
import ImageVisibilityUser from '../models/community/image_visibility_user.js';
|
||||
import GuestbookEntry from '../models/community/guestbook.js';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import DOMPurify from 'dompurify';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -33,22 +40,39 @@ class SocialNetworkService extends BaseService {
|
||||
return this.constructUserProfile(user, requestingUserId);
|
||||
}
|
||||
|
||||
async createFolder(hashedUserId, data) {
|
||||
async createFolder(hashedUserId, data, folderId) {
|
||||
await this.checkUserAccess(hashedUserId);
|
||||
const user = await User.findOne({
|
||||
hashedId: hashedUserId
|
||||
where: { hashedId: hashedUserId }
|
||||
});
|
||||
const parentFolder = Folder.findOne({
|
||||
id: data.parentId,
|
||||
userId: user.id
|
||||
});
|
||||
if (!parentFolder) {
|
||||
throw new Error('foldernotfound');
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const newFolder = await Folder.create({
|
||||
parentId: data.parentId,
|
||||
userId: user.id,
|
||||
name: data.name
|
||||
const parentFolder = data.parentId ? await Folder.findOne({
|
||||
where: { id: data.parentId, userId: user.id }
|
||||
}) : null;
|
||||
if (data.parentId && !parentFolder) {
|
||||
throw new Error('Parent folder not found');
|
||||
}
|
||||
let newFolder;
|
||||
if (folderId === 0) {
|
||||
newFolder = await Folder.create({
|
||||
parentId: data.parentId || null,
|
||||
userId: user.id,
|
||||
name: data.name
|
||||
});
|
||||
} else {
|
||||
newFolder = await Folder.findOne({
|
||||
where: { id: folderId, userId: user.id }
|
||||
});
|
||||
if (!newFolder) {
|
||||
throw new Error('Folder not found or user does not own the folder');
|
||||
}
|
||||
newFolder.name = data.name;
|
||||
await newFolder.save();
|
||||
}
|
||||
await FolderImageVisibility.destroy({
|
||||
where: { folderId: newFolder.id }
|
||||
});
|
||||
for (const visibilityId of data.visibilities) {
|
||||
await FolderImageVisibility.create({
|
||||
@@ -61,32 +85,63 @@ class SocialNetworkService extends BaseService {
|
||||
|
||||
async getFolders(hashedId) {
|
||||
const userId = await this.checkUserAccess(hashedId);
|
||||
let rootFolder = await Folder.findOne({ where: { parentId: null, userId } });
|
||||
let rootFolder = await Folder.findOne({
|
||||
where: { parentId: null, userId },
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
attributes: ['id'],
|
||||
}]
|
||||
});
|
||||
if (!rootFolder) {
|
||||
const user = await User.findOne({ where: { id: userId } });
|
||||
const visibility = await ImageVisibilityType.findOne({
|
||||
where: {
|
||||
description: 'everyone'
|
||||
}
|
||||
where: { description: 'everyone' }
|
||||
});
|
||||
rootFolder = await Folder.create({
|
||||
name: user.username,
|
||||
parentId: null,
|
||||
userId,
|
||||
userId
|
||||
});
|
||||
await FolderImageVisibility.create({
|
||||
folderId: rootFolder.id,
|
||||
visibilityTypeId: visibility.id
|
||||
});
|
||||
rootFolder = await Folder.findOne({
|
||||
where: { id: rootFolder.id },
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
attributes: ['id'],
|
||||
}]
|
||||
});
|
||||
}
|
||||
const children = await this.getSubFolders(rootFolder.id, userId);
|
||||
rootFolder = rootFolder.get();
|
||||
rootFolder = rootFolder.get();
|
||||
rootFolder.visibilityTypeIds = rootFolder.image_visibility_types.map(v => v.id);
|
||||
delete rootFolder.image_visibility_types;
|
||||
rootFolder.children = children;
|
||||
return rootFolder;
|
||||
}
|
||||
|
||||
async getSubFolders(parentId, userId) {
|
||||
const folders = await Folder.findAll({ where: { parentId, userId } });
|
||||
const folders = await Folder.findAll({
|
||||
where: { parentId, userId },
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
attributes: ['id'],
|
||||
}],
|
||||
order: [
|
||||
['name', 'asc']
|
||||
]
|
||||
});
|
||||
for (const folder of folders) {
|
||||
const children = await this.getSubFolders(folder.id, userId);
|
||||
const visibilityTypeIds = folder.image_visibility_types.map(v => v.id);
|
||||
folder.setDataValue('visibilityTypeIds', visibilityTypeIds);
|
||||
folder.setDataValue('children', children);
|
||||
folder.setDataValue('image_visibility_types', undefined);
|
||||
}
|
||||
return folders.map(folder => folder.get());
|
||||
}
|
||||
@@ -112,32 +167,56 @@ class SocialNetworkService extends BaseService {
|
||||
|
||||
async uploadImage(hashedId, file, formData) {
|
||||
const userId = await this.getUserId(hashedId);
|
||||
const newFileName = this.generateUniqueFileName(file.originalname);
|
||||
const filePath = this.buildFilePath(newFileName);
|
||||
await this.saveFile(file.buffer, filePath);
|
||||
const newImage = await this.createImageRecord(formData, userId, file, newFileName);
|
||||
const processedImageName = await this.processAndUploadUserImage(file);
|
||||
const newImage = await this.createImageRecord(formData, userId, file, processedImageName);
|
||||
await this.saveImageVisibilities(newImage.id, formData.visibility);
|
||||
return newImage;
|
||||
}
|
||||
|
||||
async processAndUploadUserImage(file) {
|
||||
try {
|
||||
const img = sharp(file.buffer);
|
||||
const metadata = await img.metadata();
|
||||
if (!metadata || !['jpeg', 'png', 'webp', 'gif'].includes(metadata.format)) {
|
||||
throw new Error('File is not a valid image');
|
||||
}
|
||||
if (metadata.width < 75 || metadata.height < 75) {
|
||||
throw new Error('Image dimensions are too small. Minimum size is 75x75 pixels.');
|
||||
}
|
||||
const resizedImg = img.resize({
|
||||
width: Math.min(metadata.width, 500),
|
||||
height: Math.min(metadata.height, 500),
|
||||
fit: sharp.fit.inside,
|
||||
withoutEnlargement: true
|
||||
});
|
||||
const newFileName = this.generateUniqueFileName(file.originalname);
|
||||
const filePath = this.buildFilePath(newFileName, 'user');
|
||||
await resizedImg.toFile(filePath);
|
||||
return newFileName;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to process image: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getUserId(hashedId) {
|
||||
return await this.checkUserAccess(hashedId);
|
||||
}
|
||||
|
||||
generateUniqueFileName(originalFileName) {
|
||||
const uniqueHash = uuidv4();
|
||||
return `${uniqueHash}`;
|
||||
return uniqueHash;
|
||||
}
|
||||
|
||||
buildFilePath(fileName) {
|
||||
const userImagesPath = path.join(__dirname, '../images/user');
|
||||
return path.join(userImagesPath, fileName);
|
||||
buildFilePath(fileName, type) {
|
||||
const basePath = path.join(__dirname, '..', 'images', type);
|
||||
return path.join(basePath, fileName);
|
||||
}
|
||||
|
||||
async saveFile(buffer, filePath) {
|
||||
try {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, buffer);
|
||||
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fsPromises.writeFile(filePath, buffer);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save file: ${error.message}`);
|
||||
}
|
||||
@@ -273,9 +352,15 @@ class SocialNetworkService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
async constructUserProfile(user, requestingUserId) {
|
||||
async constructUserProfile(user, hashedUserId) {
|
||||
const userParams = {};
|
||||
const requestingUserAge = await this.getUserAge(requestingUserId);
|
||||
const requestingUser = await User.findOne({
|
||||
where: { hashedId: hashedUserId },
|
||||
});
|
||||
if (!requestingUser) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const requestingUserAge = await this.getUserAge(requestingUser.id);
|
||||
for (const param of user.user_params) {
|
||||
const visibility = param.param_visibilities?.[0]?.visibility_type?.description || 'Invisible';
|
||||
if (visibility === 'Invisible') continue;
|
||||
@@ -340,9 +425,9 @@ class SocialNetworkService extends BaseService {
|
||||
if (!hasAccess) {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
const imagePath = this.buildFilePath(image.hash);
|
||||
const imagePath = this.buildFilePath(image.hash, 'user');
|
||||
if (!fs.existsSync(imagePath)) {
|
||||
throw new Error('File not found');
|
||||
throw new Error(`File "${imagePath}" not found`);
|
||||
}
|
||||
return imagePath;
|
||||
}
|
||||
@@ -350,7 +435,7 @@ class SocialNetworkService extends BaseService {
|
||||
async checkUserImageAccess(userId, imageId) {
|
||||
const image = await Image.findByPk(imageId);
|
||||
if (image.userId === userId) {
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
const accessRules = await ImageImageVisibility.findAll({
|
||||
where: { imageId }
|
||||
@@ -374,6 +459,258 @@ class SocialNetworkService extends BaseService {
|
||||
}
|
||||
return image.folderId;
|
||||
}
|
||||
|
||||
async getFoldersByUsername(username, hashedUserId) {
|
||||
const user = await User.findOne({ where: { username } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const requestingUserId = await this.checkUserAccess(hashedUserId);
|
||||
let rootFolder = await Folder.findOne({ where: { parentId: null, userId: user.id } });
|
||||
if (!rootFolder) {
|
||||
return null;
|
||||
}
|
||||
const accessibleFolders = await this.getAccessibleFolders(rootFolder.id, requestingUserId);
|
||||
rootFolder = rootFolder.get();
|
||||
rootFolder.children = accessibleFolders;
|
||||
return rootFolder;
|
||||
}
|
||||
|
||||
async getAccessibleFolders(folderId, requestingUserId) {
|
||||
const folderIdString = String(folderId);
|
||||
const requestingUserIdString = String(requestingUserId);
|
||||
const requestingUser = await User.findOne({ where: { id: requestingUserIdString } });
|
||||
const isAdult = this.isUserAdult(requestingUser.id);
|
||||
const accessibleFolders = await Folder.findAll({
|
||||
where: { parentId: folderIdString },
|
||||
include: [
|
||||
{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ description: 'everyone' },
|
||||
{ description: 'adults', ...(isAdult ? {} : { [Op.not]: null }) },
|
||||
{ description: 'friends-and-adults', ...(isAdult ? {} : { [Op.not]: null }) }
|
||||
]
|
||||
},
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: ImageVisibilityUser,
|
||||
through: { model: FolderVisibilityUser },
|
||||
where: { user_id: requestingUserIdString },
|
||||
required: false
|
||||
}
|
||||
]
|
||||
});
|
||||
const folderList = [];
|
||||
for (let folder of accessibleFolders) {
|
||||
const children = await this.getAccessibleFolders(folder.id, requestingUserIdString);
|
||||
folder = folder.get();
|
||||
folder.children = children;
|
||||
folderList.push(folder);
|
||||
}
|
||||
return folderList;
|
||||
}
|
||||
|
||||
async deleteFolder(hashedUserId, folderId) {
|
||||
const userId = await this.checkUserAccess(hashedUserId);
|
||||
const folder = await Folder.findOne({
|
||||
where: { id: folderId, userId }
|
||||
});
|
||||
if (!folder) {
|
||||
throw new Error('Folder not found or access denied');
|
||||
}
|
||||
await FolderImageVisibility.destroy({ where: { folderId: folder.id } });
|
||||
await folder.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
async createGuestbookEntry(hashedSenderId, recipientName, htmlContent, image) {
|
||||
const sender = await User.findOne({ where: { hashedId: hashedSenderId } });
|
||||
if (!sender) {
|
||||
throw new Error('Sender not found');
|
||||
}
|
||||
const recipient = await User.findOne({ where: { username: recipientName } });
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
const sanitizedContent = this.sanitizeHtml(htmlContent);
|
||||
let uploadedImage = null;
|
||||
if (image) {
|
||||
uploadedImage = await this.processAndUploadGuestbookImage(image);
|
||||
}
|
||||
const entry = await GuestbookEntry.create({
|
||||
senderId: sender.id,
|
||||
recipientId: recipient.id,
|
||||
senderUsername: sender.username,
|
||||
contentHtml: sanitizedContent,
|
||||
imageUrl: uploadedImage
|
||||
});
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
sanitizeHtml(htmlContent) {
|
||||
const window = new JSDOM('').window;
|
||||
const purify = DOMPurify(window);
|
||||
const cleanHtml = purify.sanitize(htmlContent, {
|
||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li', 'br'],
|
||||
ALLOWED_ATTR: ['href', 'title'],
|
||||
FORBID_TAGS: ['script', 'style', 'iframe', 'img'],
|
||||
ALLOWED_URI_REGEXP: /^https?:\/\//,
|
||||
});
|
||||
|
||||
return cleanHtml;
|
||||
}
|
||||
|
||||
async processAndUploadGuestbookImage(image) {
|
||||
try {
|
||||
const img = sharp(image.buffer);
|
||||
const metadata = await img.metadata();
|
||||
if (!metadata || !['jpeg', 'png', 'webp', 'gif'].includes(metadata.format)) {
|
||||
throw new Error('File is not a valid image');
|
||||
}
|
||||
if (metadata.width < 20 || metadata.height < 20) {
|
||||
throw new Error('Image dimensions are too small. Minimum size is 20x20 pixels.');
|
||||
}
|
||||
const resizedImg = img.resize({
|
||||
width: Math.min(metadata.width, 500),
|
||||
height: Math.min(metadata.height, 500),
|
||||
fit: sharp.fit.inside,
|
||||
withoutEnlargement: true
|
||||
});
|
||||
const newFileName = this.generateUniqueFileName(image.originalname);
|
||||
const filePath = this.buildFilePath(newFileName, 'guestbook');
|
||||
await resizedImg.toFile(filePath);
|
||||
return newFileName;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to process image: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getGuestbookEntries(hashedUserId, username, page = 1) {
|
||||
const pageSize = 20;
|
||||
const offset = (page - 1) * pageSize;
|
||||
this.checkUserAccess(hashedUserId);
|
||||
const user = await User.findOne({ where: { username: username } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const entries = await GuestbookEntry.findAndCountAll({
|
||||
where: { recipientId: user.id },
|
||||
include: [
|
||||
{ model: User, as: 'sender', attributes: ['username'] },
|
||||
],
|
||||
limit: pageSize,
|
||||
offset: offset,
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
const resultList = entries.rows.map(entry => ({
|
||||
id: entry.id,
|
||||
sender: entry.sender ? entry.sender.username : entry.senderUsername,
|
||||
contentHtml: entry.contentHtml,
|
||||
withImage: entry.imageUrl !== null,
|
||||
createdAt: entry.createdAt
|
||||
}));
|
||||
return {
|
||||
entries: resultList,
|
||||
currentPage: page,
|
||||
totalPages: Math.ceil(entries.count / pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
async getGuestbookImageFilePath(hashedUserId, guestbookOwnerName, entryId) {
|
||||
await this.checkUserAccess(hashedUserId);
|
||||
|
||||
const guestbookOwner = await User.findOne({
|
||||
where: { username: guestbookOwnerName },
|
||||
});
|
||||
if (!guestbookOwner) {
|
||||
throw new Error('usernotfound');
|
||||
}
|
||||
const entry = await GuestbookEntry.findOne({
|
||||
where: { id: entryId, recipientId: guestbookOwner.id },
|
||||
});
|
||||
if (!entry) {
|
||||
throw new Error('entrynotfound');
|
||||
}
|
||||
if (!entry.imageUrl) {
|
||||
console.log(entry);
|
||||
throw new Error('entryhasnoimage');
|
||||
}
|
||||
console.log(`Image URL: ${entry.imageUrl}`);
|
||||
const imagePath = this.buildFilePath(entry.imageUrl, 'guestbook');
|
||||
if (!fs.existsSync(imagePath)) {
|
||||
throw new Error(imagenotfound);
|
||||
}
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
async deleteGuestbookEntry(hashedUserId, entryId) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const entry = await GuestbookEntry.findOne({ where: { id: entryId } });
|
||||
if (!entry) {
|
||||
throw new Error('Entry not found');
|
||||
}
|
||||
if (entry.senderId !== user.id && entry.recipientId !== user.id) {
|
||||
throw new Error('Not authorized to delete this entry');
|
||||
}
|
||||
await entry.destroy();
|
||||
}
|
||||
|
||||
async createDiaryEntry(userId, text) {
|
||||
const newEntry = await Diary.create({
|
||||
userId: userId,
|
||||
text: text,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
return newEntry;
|
||||
}
|
||||
|
||||
async updateDiaryEntry(diaryId, userId, newText) {
|
||||
const existingEntry = await Diary.findOne({
|
||||
where: { id: diaryId, userId: userId }
|
||||
});
|
||||
if (!existingEntry) {
|
||||
throw new Error('Diary entry not found or unauthorized access');
|
||||
}
|
||||
await DiaryHistory.create({
|
||||
diaryId: existingEntry.id,
|
||||
userId: existingEntry.userId,
|
||||
oldText: existingEntry.text,
|
||||
oldCreatedAt: existingEntry.createdAt,
|
||||
oldUpdatedAt: existingEntry.updatedAt,
|
||||
});
|
||||
existingEntry.text = newText;
|
||||
existingEntry.updatedAt = new Date();
|
||||
await existingEntry.save();
|
||||
return existingEntry;
|
||||
}
|
||||
|
||||
async deleteDiaryEntry(diaryId, userId) {
|
||||
const entryToDelete = await Diary.findOne({
|
||||
where: { id: diaryId, userId: userId }
|
||||
});
|
||||
if (!entryToDelete) {
|
||||
throw new Error('Diary entry not found or unauthorized access');
|
||||
}
|
||||
await entryToDelete.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
async getDiaryEntries(userId) {
|
||||
const entries = await Diary.findAll({
|
||||
where: { userId: userId },
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
export default SocialNetworkService;
|
||||
export default SocialNetworkService;
|
||||
@@ -30,4 +30,4 @@ const syncModels = async (models) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { sequelize, initializeDatabase };
|
||||
export { sequelize, initializeDatabase, syncModels };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { initializeDatabase } from './sequelize.js';
|
||||
import { initializeDatabase, syncModels } from './sequelize.js';
|
||||
import initializeTypes from './initializeTypes.js';
|
||||
import initializeSettings from './initializeSettings.js';
|
||||
import initializeUserRights from './initializeUserRights.js';
|
||||
@@ -10,10 +10,8 @@ import { createTriggers } from '../models/trigger.js';
|
||||
const syncDatabase = async () => {
|
||||
try {
|
||||
await initializeDatabase();
|
||||
await syncModels(models);
|
||||
setupAssociations();
|
||||
for (const model of Object.values(models)) {
|
||||
await model.sync();
|
||||
}
|
||||
createTriggers();
|
||||
|
||||
await initializeSettings();
|
||||
|
||||
Reference in New Issue
Block a user