fixed project slug uniqueness issue

This commit is contained in:
Dmitri 2026-03-31 13:03:50 +04:00
parent 91c24165bf
commit a9a2866b23
3 changed files with 66 additions and 2 deletions

View File

@ -34,6 +34,7 @@ const errors = {
revokingOwnPermission: `You can't revoke your own owner permission`, revokingOwnPermission: `You can't revoke your own owner permission`,
deletingHimself: `You can't delete yourself`, deletingHimself: `You can't delete yourself`,
emailRequired: 'Email is required', emailRequired: 'Email is required',
slugAlreadyExists: 'This slug is already in use by another project',
}, },
}, },

View File

@ -43,9 +43,47 @@ module.exports = class ProjectsService {
return uniqueSlug; return uniqueSlug;
} }
/**
* Validate slug uniqueness before create/update
* @param {string} slug - Slug to validate
* @param {string|null} excludeId - Project ID to exclude (for updates)
* @param {Transaction} transaction - DB transaction
* @throws {ValidationError} if slug already exists
* @returns {string} Normalized slug
*/
static async validateSlugUniqueness(slug, excludeId, transaction) {
const normalizedSlug = ProjectsService.normalizeSlug(slug);
const whereClause = { slug: normalizedSlug };
if (excludeId) {
whereClause.id = { [db.Sequelize.Op.ne]: excludeId };
}
const existing = await db.projects.findOne({
where: whereClause,
paranoid: false,
transaction,
});
if (existing) {
throw new ValidationError('iam.errors.slugAlreadyExists');
}
return normalizedSlug;
}
static async create(data, currentUser) { static async create(data, currentUser) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
// Validate slug uniqueness if provided
if (data.slug) {
data.slug = await ProjectsService.validateSlugUniqueness(
data.slug,
null,
transaction,
);
}
const createdProject = await ProjectsDBApi.create(data, { const createdProject = await ProjectsDBApi.create(data, {
currentUser, currentUser,
transaction, transaction,
@ -196,6 +234,15 @@ module.exports = class ProjectsService {
throw new ValidationError('projectsNotFound'); throw new ValidationError('projectsNotFound');
} }
// Validate slug uniqueness if slug is being changed
if (data.slug && data.slug !== projects.slug) {
data.slug = await ProjectsService.validateSlugUniqueness(
data.slug,
id,
transaction,
);
}
const updatedProjects = await ProjectsDBApi.update(id, data, { const updatedProjects = await ProjectsDBApi.update(id, data, {
currentUser, currentUser,
transaction, transaction,

View File

@ -24,6 +24,7 @@ import BaseButton from '../../components/BaseButton';
import { deleteItem, update, fetch } from '../../stores/projects/projectsSlice'; import { deleteItem, update, fetch } from '../../stores/projects/projectsSlice';
import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { toast, ToastContainer } from 'react-toastify';
import type { Project } from '../../types/entities'; import type { Project } from '../../types/entities';
import { logger } from '../../lib/logger'; import { logger } from '../../lib/logger';
@ -143,8 +144,22 @@ const EditProjectsPage = () => {
og_image_url: data.og_image_url, og_image_url: data.og_image_url,
}; };
await dispatch(update({ id: id as string, data: apiData })); try {
await router.push('/projects/projects-list'); await dispatch(update({ id: id as string, data: apiData })).unwrap();
toast('Project settings saved', {
type: 'success',
position: 'bottom-center',
});
} catch (error: unknown) {
const errorMessage =
error && typeof error === 'object' && 'message' in error
? String((error as { message: string }).message)
: 'Failed to save project settings';
toast(errorMessage, {
type: 'error',
position: 'bottom-center',
});
}
}; };
const handleDelete = async () => { const handleDelete = async () => {
@ -322,6 +337,7 @@ const EditProjectsPage = () => {
)} )}
</Formik> </Formik>
</CardBox> </CardBox>
<ToastContainer />
</SectionMain> </SectionMain>
</> </>
); );