* Refactor swagger docs * Scaffold refactor * Refactor crud tasks * fixup! Refactor crud tasks * Break down task layout * fixup! Break down task layout * fixup! fixup! Break down task layout * Cleanup comments * fixup! Cleanup comments * Cleanup obsolete code * Remove helpers
410 lines
13 KiB
JavaScript
410 lines
13 KiB
JavaScript
const express = require('express');
|
|
const { Note, Tag, Project } = require('../models');
|
|
const { extractUidFromSlug, isValidUid } = require('../utils/slug-utils');
|
|
const { validateTagName } = require('../services/tagsService');
|
|
const router = express.Router();
|
|
const { getAuthenticatedUserId } = require('../utils/request-utils');
|
|
|
|
router.use((req, res, next) => {
|
|
const userId = getAuthenticatedUserId(req);
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
req.authUserId = userId;
|
|
next();
|
|
});
|
|
const permissionsService = require('../services/permissionsService');
|
|
const { hasAccess } = require('../middleware/authorize');
|
|
const _ = require('lodash');
|
|
const { logError } = require('../services/logService');
|
|
|
|
// Helper function to update note tags
|
|
async function updateNoteTags(note, tagsArray, userId) {
|
|
if (_.isEmpty(tagsArray)) {
|
|
await note.setTags([]);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Validate and filter tag names
|
|
const validTagNames = [];
|
|
const invalidTags = [];
|
|
|
|
for (const name of tagsArray) {
|
|
const validation = validateTagName(name);
|
|
if (validation.valid) {
|
|
// Check for duplicates
|
|
if (!validTagNames.includes(validation.name)) {
|
|
validTagNames.push(validation.name);
|
|
}
|
|
} else {
|
|
invalidTags.push({ name, error: validation.error });
|
|
}
|
|
}
|
|
|
|
if (invalidTags.length > 0) {
|
|
throw new Error(
|
|
`Invalid tag names: ${invalidTags.map((t) => `"${t.name}" (${t.error})`).join(', ')}`
|
|
);
|
|
}
|
|
|
|
const tags = await Promise.all(
|
|
validTagNames.map(async (name) => {
|
|
const [tag] = await Tag.findOrCreate({
|
|
where: { name, user_id: userId },
|
|
defaults: { name, user_id: userId },
|
|
});
|
|
return tag;
|
|
})
|
|
);
|
|
await note.setTags(tags);
|
|
} catch (error) {
|
|
logError('Failed to update tags:', error.message);
|
|
throw error; // Re-throw to handle at route level
|
|
}
|
|
}
|
|
|
|
router.get('/notes', async (req, res) => {
|
|
try {
|
|
const orderBy = req.query.order_by || 'title:asc';
|
|
const [orderColumn, orderDirection] = orderBy.split(':');
|
|
|
|
const whereClause = await permissionsService.ownershipOrPermissionWhere(
|
|
'note',
|
|
req.authUserId
|
|
);
|
|
let includeClause = [
|
|
{
|
|
model: Tag,
|
|
attributes: ['name', 'uid'],
|
|
through: { attributes: [] },
|
|
},
|
|
{
|
|
model: Project,
|
|
required: false,
|
|
attributes: ['name', 'uid'],
|
|
},
|
|
];
|
|
|
|
// Filter by tag
|
|
if (req.query.tag) {
|
|
includeClause[0].where = { name: req.query.tag };
|
|
includeClause[0].required = true;
|
|
}
|
|
|
|
const notes = await Note.findAll({
|
|
where: whereClause,
|
|
include: includeClause,
|
|
order: [[orderColumn, orderDirection.toUpperCase()]],
|
|
distinct: true,
|
|
});
|
|
|
|
res.json(notes);
|
|
} catch (error) {
|
|
logError('Error fetching notes:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.get(
|
|
'/note/:uidSlug',
|
|
hasAccess(
|
|
'ro',
|
|
'note',
|
|
async (req) => {
|
|
const uid = extractUidFromSlug(req.params.uidSlug);
|
|
// Check if note exists - return null if it doesn't (triggers 404)
|
|
const note = await Note.findOne({
|
|
where: { uid },
|
|
attributes: ['uid'],
|
|
});
|
|
return note ? note.uid : null;
|
|
},
|
|
{ notFoundMessage: 'Note not found.' }
|
|
),
|
|
async (req, res) => {
|
|
try {
|
|
const note = await Note.findOne({
|
|
where: { uid: extractUidFromSlug(req.params.uidSlug) },
|
|
include: [
|
|
{
|
|
model: Tag,
|
|
attributes: ['name', 'uid'],
|
|
through: { attributes: [] },
|
|
},
|
|
{
|
|
model: Project,
|
|
required: false,
|
|
attributes: ['name', 'uid'],
|
|
},
|
|
],
|
|
});
|
|
|
|
res.json(note);
|
|
} catch (error) {
|
|
logError('Error fetching note:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
);
|
|
|
|
router.post('/note', async (req, res) => {
|
|
try {
|
|
const { title, content, project_uid, project_id, tags, color } =
|
|
req.body;
|
|
|
|
const noteAttributes = {
|
|
title,
|
|
content,
|
|
user_id: req.authUserId,
|
|
};
|
|
|
|
// Add color if provided
|
|
if (color !== undefined) {
|
|
noteAttributes.color = color;
|
|
}
|
|
|
|
// Support both project_uid (new) and project_id (legacy)
|
|
const projectIdentifier = project_uid || project_id;
|
|
|
|
// If project identifier is provided, validate access and assign
|
|
if (
|
|
projectIdentifier &&
|
|
!_.isEmpty(projectIdentifier.toString().trim())
|
|
) {
|
|
let project;
|
|
|
|
// Try to find by UID first (new way), then by ID (legacy)
|
|
if (project_uid) {
|
|
const projectUidValue = project_uid.toString().trim();
|
|
project = await Project.findOne({
|
|
where: { uid: projectUidValue },
|
|
});
|
|
} else {
|
|
// Legacy: find by numeric ID
|
|
project = await Project.findByPk(project_id);
|
|
}
|
|
|
|
if (!project) {
|
|
return res
|
|
.status(404)
|
|
.json({ error: 'Note project not found' });
|
|
}
|
|
|
|
// Check if user has write access to the project
|
|
const projectAccess = await permissionsService.getAccess(
|
|
req.authUserId,
|
|
'project',
|
|
project.uid
|
|
);
|
|
const isOwner = project.user_id === req.authUserId;
|
|
const canWrite =
|
|
isOwner || projectAccess === 'rw' || projectAccess === 'admin';
|
|
|
|
if (!canWrite) {
|
|
return res.status(403).json({ error: 'Forbidden' });
|
|
}
|
|
|
|
noteAttributes.project_id = project.id;
|
|
}
|
|
|
|
const note = await Note.create(noteAttributes);
|
|
|
|
// Handle tags - can be an array of strings
|
|
// or array of objects with name property
|
|
let tagNames = [];
|
|
if (Array.isArray(tags)) {
|
|
if (tags.every((t) => typeof t === 'string')) {
|
|
tagNames = tags;
|
|
} else if (tags.every((t) => typeof t === 'object' && t.name)) {
|
|
tagNames = tags.map((t) => t.name);
|
|
}
|
|
}
|
|
|
|
await updateNoteTags(note, tagNames, req.authUserId);
|
|
|
|
// Reload note with associations
|
|
const noteWithAssociations = await Note.findByPk(note.id, {
|
|
include: [
|
|
{
|
|
model: Tag,
|
|
attributes: ['name', 'uid'],
|
|
through: { attributes: [] },
|
|
},
|
|
{
|
|
model: Project,
|
|
required: false,
|
|
attributes: ['name', 'uid'],
|
|
},
|
|
],
|
|
});
|
|
|
|
res.status(201).json({
|
|
...noteWithAssociations.toJSON(),
|
|
uid: noteWithAssociations.uid,
|
|
});
|
|
} catch (error) {
|
|
logError('Error creating note:', error);
|
|
res.status(400).json({
|
|
error: 'There was a problem creating the note.',
|
|
details: error.errors
|
|
? error.errors.map((e) => e.message)
|
|
: [error.message],
|
|
});
|
|
}
|
|
});
|
|
|
|
router.patch(
|
|
'/note/:uid',
|
|
hasAccess(
|
|
'rw',
|
|
'note',
|
|
async (req) => {
|
|
const uid = extractUidFromSlug(req.params.uid);
|
|
// Check if note exists - return null if it doesn't (triggers 404)
|
|
const note = await Note.findOne({
|
|
where: { uid },
|
|
attributes: ['uid'],
|
|
});
|
|
return note ? note.uid : null;
|
|
},
|
|
{ notFoundMessage: 'Note not found.' }
|
|
),
|
|
async (req, res) => {
|
|
try {
|
|
const note = await Note.findOne({
|
|
where: { uid: req.params.uid },
|
|
});
|
|
|
|
const { title, content, project_uid, project_id, tags, color } =
|
|
req.body;
|
|
|
|
const updateData = {};
|
|
if (title !== undefined) updateData.title = title;
|
|
if (content !== undefined) updateData.content = content;
|
|
if (color !== undefined) updateData.color = color;
|
|
|
|
// Handle project assignment - support both project_uid (new) and project_id (legacy)
|
|
const projectIdentifier =
|
|
project_uid !== undefined ? project_uid : project_id;
|
|
|
|
if (projectIdentifier !== undefined) {
|
|
if (projectIdentifier && projectIdentifier.toString().trim()) {
|
|
let project;
|
|
|
|
// Try to find by UID first (new way), then by ID (legacy)
|
|
if (
|
|
project_uid !== undefined &&
|
|
typeof project_uid === 'string'
|
|
) {
|
|
const projectUidValue = project_uid.trim();
|
|
project = await Project.findOne({
|
|
where: { uid: projectUidValue },
|
|
});
|
|
} else if (project_id !== undefined) {
|
|
// Legacy: find by numeric ID
|
|
project = await Project.findByPk(project_id);
|
|
}
|
|
|
|
if (!project) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: 'Invalid project.' });
|
|
}
|
|
const projectAccess = await permissionsService.getAccess(
|
|
req.authUserId,
|
|
'project',
|
|
project.uid
|
|
);
|
|
const isOwner = project.user_id === req.authUserId;
|
|
const canWrite =
|
|
isOwner ||
|
|
projectAccess === 'rw' ||
|
|
projectAccess === 'admin';
|
|
if (!canWrite) {
|
|
return res.status(403).json({ error: 'Forbidden' });
|
|
}
|
|
updateData.project_id = project.id;
|
|
} else {
|
|
updateData.project_id = null;
|
|
}
|
|
}
|
|
|
|
await note.update(updateData);
|
|
|
|
// Handle tags if provided
|
|
if (tags !== undefined) {
|
|
let tagNames = [];
|
|
if (Array.isArray(tags)) {
|
|
if (tags.every((t) => typeof t === 'string')) {
|
|
tagNames = tags;
|
|
} else if (
|
|
tags.every((t) => typeof t === 'object' && t.name)
|
|
) {
|
|
tagNames = tags.map((t) => t.name);
|
|
}
|
|
}
|
|
await updateNoteTags(note, tagNames, req.authUserId);
|
|
}
|
|
|
|
// Reload note with associations
|
|
const noteWithAssociations = await Note.findByPk(note.id, {
|
|
include: [
|
|
{
|
|
model: Tag,
|
|
attributes: ['id', 'name', 'uid'],
|
|
through: { attributes: [] },
|
|
},
|
|
{
|
|
model: Project,
|
|
required: false,
|
|
attributes: ['id', 'name', 'uid'],
|
|
},
|
|
],
|
|
});
|
|
|
|
res.json(noteWithAssociations);
|
|
} catch (error) {
|
|
logError('Error updating note:', error);
|
|
res.status(400).json({
|
|
error: 'There was a problem updating the note.',
|
|
details: error.errors
|
|
? error.errors.map((e) => e.message)
|
|
: [error.message],
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
router.delete(
|
|
'/note/:uid',
|
|
hasAccess(
|
|
'rw',
|
|
'note',
|
|
async (req) => {
|
|
const uid = extractUidFromSlug(req.params.uid);
|
|
// Check if note exists - return null if it doesn't (triggers 404)
|
|
const note = await Note.findOne({
|
|
where: { uid },
|
|
attributes: ['uid'],
|
|
});
|
|
return note ? note.uid : null;
|
|
},
|
|
{ notFoundMessage: 'Note not found.' }
|
|
),
|
|
async (req, res) => {
|
|
try {
|
|
const note = await Note.findOne({
|
|
where: { uid: req.params.uid },
|
|
});
|
|
|
|
await note.destroy();
|
|
res.json({ message: 'Note deleted successfully.' });
|
|
} catch (error) {
|
|
logError('Error deleting note:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|
|
);
|
|
|
|
module.exports = router;
|