Merge pull request #439 from chrisvel/fix-project-sharing
Fix an issue with tasks permissions on shared project
This commit is contained in:
commit
ba34e2f3f4
9 changed files with 537 additions and 27 deletions
|
|
@ -18,6 +18,13 @@ const requireAuth = async (req, res, next) => {
|
|||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
// Debug logging to verify correct user is authenticated
|
||||
if (req.path.includes('/tasks') && req.method === 'GET') {
|
||||
console.log(
|
||||
`[AUTH DEBUG] ${req.method} ${req.path} - User: ${user.email} (ID: ${user.id})`
|
||||
);
|
||||
}
|
||||
|
||||
req.currentUser = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,21 @@ router.get('/task/:uid/timeline', async (req, res) => {
|
|||
if (!isValidUid(req.params.uid))
|
||||
return res.status(400).json({ error: 'Invalid UID' });
|
||||
|
||||
const permissionsService = require('../services/permissionsService');
|
||||
|
||||
// Check if user has access to the task (either owns it or has access through shared project)
|
||||
const access = await permissionsService.getAccess(
|
||||
req.currentUser.id,
|
||||
'task',
|
||||
req.params.uid
|
||||
);
|
||||
|
||||
if (access === 'none') {
|
||||
return res.status(404).json({ error: 'Task not found' });
|
||||
}
|
||||
|
||||
const task = await Task.findOne({
|
||||
where: { uid: req.params.uid, user_id: req.currentUser.id },
|
||||
where: { uid: req.params.uid },
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
|
|
@ -39,8 +52,21 @@ router.get('/task/:uid/completion-time', async (req, res) => {
|
|||
if (!isValidUid(req.params.uid))
|
||||
return res.status(400).json({ error: 'Invalid UID' });
|
||||
|
||||
const permissionsService = require('../services/permissionsService');
|
||||
|
||||
// Check if user has access to the task (either owns it or has access through shared project)
|
||||
const access = await permissionsService.getAccess(
|
||||
req.currentUser.id,
|
||||
'task',
|
||||
req.params.uid
|
||||
);
|
||||
|
||||
if (access === 'none') {
|
||||
return res.status(404).json({ error: 'Task not found' });
|
||||
}
|
||||
|
||||
const task = await Task.findOne({
|
||||
where: { uid: req.params.uid, user_id: req.currentUser.id },
|
||||
where: { uid: req.params.uid },
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
|
|
|
|||
|
|
@ -838,24 +838,28 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
|
||||
const tasksDueToday = await Task.findAll({
|
||||
where: {
|
||||
...visibleTasksWhere,
|
||||
status: {
|
||||
[Op.notIn]: [
|
||||
Task.STATUS.DONE,
|
||||
Task.STATUS.ARCHIVED,
|
||||
'done',
|
||||
'archived',
|
||||
],
|
||||
},
|
||||
parent_task_id: null, // Exclude subtasks
|
||||
recurring_parent_id: null, // Exclude recurring instances
|
||||
[Op.or]: [
|
||||
{ due_date: { [Op.lte]: todayBounds.end } },
|
||||
sequelize.literal(`EXISTS (
|
||||
SELECT 1 FROM projects
|
||||
WHERE projects.id = Task.project_id
|
||||
[Op.and]: [
|
||||
visibleTasksWhere,
|
||||
{
|
||||
status: {
|
||||
[Op.notIn]: [
|
||||
Task.STATUS.DONE,
|
||||
Task.STATUS.ARCHIVED,
|
||||
'done',
|
||||
'archived',
|
||||
],
|
||||
},
|
||||
parent_task_id: null, // Exclude subtasks
|
||||
recurring_parent_id: null, // Exclude recurring instances
|
||||
[Op.or]: [
|
||||
{ due_date: { [Op.lte]: todayBounds.end } },
|
||||
sequelize.literal(`EXISTS (
|
||||
SELECT 1 FROM projects
|
||||
WHERE projects.id = Task.project_id
|
||||
AND projects.due_date_at <= '${todayBounds.end.toISOString()}'
|
||||
)`),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
include: [
|
||||
|
|
|
|||
|
|
@ -29,19 +29,57 @@ async function getAccess(userId, resourceType, resourceUid) {
|
|||
} else if (resourceType === 'task') {
|
||||
const t = await Task.findOne({
|
||||
where: { uid: resourceUid },
|
||||
attributes: ['user_id'],
|
||||
attributes: ['user_id', 'project_id'],
|
||||
raw: true,
|
||||
});
|
||||
if (!t) return ACCESS.NONE;
|
||||
if (t.user_id === userId) return ACCESS.RW;
|
||||
|
||||
// Check if user has access through the parent project
|
||||
if (t.project_id) {
|
||||
const project = await Project.findOne({
|
||||
where: { id: t.project_id },
|
||||
attributes: ['uid'],
|
||||
raw: true,
|
||||
});
|
||||
if (project) {
|
||||
const projectAccess = await getAccess(
|
||||
userId,
|
||||
'project',
|
||||
project.uid
|
||||
);
|
||||
if (projectAccess !== ACCESS.NONE) {
|
||||
return projectAccess; // Inherit access from project
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (resourceType === 'note') {
|
||||
const n = await Note.findOne({
|
||||
where: { uid: resourceUid },
|
||||
attributes: ['user_id'],
|
||||
attributes: ['user_id', 'project_id'],
|
||||
raw: true,
|
||||
});
|
||||
if (!n) return ACCESS.NONE;
|
||||
if (n.user_id === userId) return ACCESS.RW;
|
||||
|
||||
// Check if user has access through the parent project
|
||||
if (n.project_id) {
|
||||
const project = await Project.findOne({
|
||||
where: { id: n.project_id },
|
||||
attributes: ['uid'],
|
||||
raw: true,
|
||||
});
|
||||
if (project) {
|
||||
const projectAccess = await getAccess(
|
||||
userId,
|
||||
'project',
|
||||
project.uid
|
||||
);
|
||||
if (projectAccess !== ACCESS.NONE) {
|
||||
return projectAccess; // Inherit access from project
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shared
|
||||
|
|
@ -59,11 +97,83 @@ async function getAccess(userId, resourceType, resourceUid) {
|
|||
|
||||
async function ownershipOrPermissionWhere(resourceType, userId) {
|
||||
// Admin users can see all resources
|
||||
if (await isAdmin(userId)) {
|
||||
// Note: isAdmin expects a UID, but we might receive a numeric ID
|
||||
// Get the user's UID if we received a numeric ID
|
||||
let userUid = userId;
|
||||
if (typeof userId === 'number' || !isNaN(parseInt(userId))) {
|
||||
const { User } = require('../models');
|
||||
const user = await User.findByPk(userId, {
|
||||
attributes: ['uid', 'email'],
|
||||
});
|
||||
if (user) {
|
||||
userUid = user.uid;
|
||||
console.log(
|
||||
`[PERMISSIONS DEBUG] User lookup: ID=${userId}, UID=${userUid}, Email=${user.email}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const isUserAdmin = await isAdmin(userUid);
|
||||
console.log(
|
||||
`[PERMISSIONS DEBUG] Resource: ${resourceType}, UserId: ${userId}, IsAdmin: ${isUserAdmin}`
|
||||
);
|
||||
|
||||
if (isUserAdmin) {
|
||||
console.log(
|
||||
`[PERMISSIONS DEBUG] User is admin, returning empty where clause (all resources visible)`
|
||||
);
|
||||
return {}; // empty where clause = no restriction
|
||||
}
|
||||
|
||||
const sharedUids = await getSharedUidsForUser(resourceType, userId);
|
||||
console.log(
|
||||
`[PERMISSIONS DEBUG] Shared ${resourceType} UIDs for user ${userId}:`,
|
||||
sharedUids
|
||||
);
|
||||
|
||||
// For tasks and notes, also include items from shared projects
|
||||
if (resourceType === 'task' || resourceType === 'note') {
|
||||
const sharedProjectUids = await getSharedUidsForUser('project', userId);
|
||||
console.log(
|
||||
`[PERMISSIONS DEBUG] Shared project UIDs for user ${userId}:`,
|
||||
sharedProjectUids
|
||||
);
|
||||
|
||||
// Get the project IDs for shared projects
|
||||
let sharedProjectIds = [];
|
||||
if (sharedProjectUids.length > 0) {
|
||||
const projects = await Project.findAll({
|
||||
where: { uid: { [Op.in]: sharedProjectUids } },
|
||||
attributes: ['id'],
|
||||
raw: true,
|
||||
});
|
||||
sharedProjectIds = projects.map((p) => p.id);
|
||||
console.log(
|
||||
`[PERMISSIONS DEBUG] Shared project IDs for user ${userId}:`,
|
||||
sharedProjectIds
|
||||
);
|
||||
}
|
||||
|
||||
const conditions = [
|
||||
{ user_id: userId }, // Items owned by user
|
||||
];
|
||||
|
||||
if (sharedUids.length > 0) {
|
||||
conditions.push({ uid: { [Op.in]: sharedUids } }); // Items directly shared with user
|
||||
}
|
||||
|
||||
if (sharedProjectIds.length > 0) {
|
||||
conditions.push({ project_id: { [Op.in]: sharedProjectIds } }); // Items in shared projects
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[PERMISSIONS DEBUG] Final where conditions for ${resourceType}:`,
|
||||
JSON.stringify(conditions)
|
||||
);
|
||||
return { [Op.or]: conditions };
|
||||
}
|
||||
|
||||
// For other resource types (projects, etc.), use the original logic
|
||||
return {
|
||||
[Op.or]: [
|
||||
{ user_id: userId },
|
||||
|
|
|
|||
346
backend/tests/integration/project-sharing.test.js
Normal file
346
backend/tests/integration/project-sharing.test.js
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
const request = require('supertest');
|
||||
const app = require('../../app');
|
||||
const {
|
||||
User,
|
||||
Project,
|
||||
Task,
|
||||
Note,
|
||||
Permission,
|
||||
sequelize,
|
||||
} = require('../../models');
|
||||
const { createTestUser } = require('../helpers/testUtils');
|
||||
|
||||
describe('Project Sharing Integration Tests', () => {
|
||||
let ownerUser, sharedUser, ownerAgent, sharedUserAgent, project;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test users using test helper
|
||||
ownerUser = await createTestUser({
|
||||
email: `owner_${Date.now()}@test.com`,
|
||||
name: 'Owner',
|
||||
timezone: 'UTC',
|
||||
});
|
||||
|
||||
sharedUser = await createTestUser({
|
||||
email: `shared_${Date.now()}@test.com`,
|
||||
name: 'Shared User',
|
||||
timezone: 'UTC',
|
||||
});
|
||||
|
||||
// Create agents for both users (maintains sessions)
|
||||
ownerAgent = request.agent(app);
|
||||
sharedUserAgent = request.agent(app);
|
||||
|
||||
// Login as owner
|
||||
await ownerAgent
|
||||
.post('/api/login')
|
||||
.send({ email: ownerUser.email, password: 'password123' });
|
||||
|
||||
// Login as shared user
|
||||
await sharedUserAgent
|
||||
.post('/api/login')
|
||||
.send({ email: sharedUser.email, password: 'password123' });
|
||||
|
||||
// Create a project as owner
|
||||
const projectResponse = await ownerAgent.post('/api/project').send({
|
||||
name: 'Shared Test Project',
|
||||
description: 'Project for sharing tests',
|
||||
});
|
||||
project = projectResponse.body;
|
||||
|
||||
// Share the project with read-write access
|
||||
await ownerAgent.post('/api/shares').send({
|
||||
resource_type: 'project',
|
||||
resource_uid: project.uid,
|
||||
target_user_email: sharedUser.email,
|
||||
access_level: 'rw',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await sequelize.close();
|
||||
});
|
||||
|
||||
describe('Issue 1: Task and Note Visibility in Shared Projects', () => {
|
||||
test('shared user should see tasks in shared project', async () => {
|
||||
// Owner creates a task in the shared project
|
||||
const taskResponse = await ownerAgent.post('/api/task').send({
|
||||
name: 'Task by owner in shared project',
|
||||
project_id: project.id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
const taskInSharedProject = taskResponse.body;
|
||||
|
||||
// Shared user should see this task
|
||||
const response = await sharedUserAgent.get('/api/tasks');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.tasks).toBeDefined();
|
||||
|
||||
const foundTask = response.body.tasks.find(
|
||||
(t) => t.id === taskInSharedProject.id
|
||||
);
|
||||
expect(foundTask).toBeDefined();
|
||||
expect(foundTask.name).toBe('Task by owner in shared project');
|
||||
});
|
||||
|
||||
test('shared user should see notes in shared project', async () => {
|
||||
// Owner creates a note in the shared project
|
||||
const noteResponse = await ownerAgent.post('/api/note').send({
|
||||
title: 'Note by owner in shared project',
|
||||
content: 'This note should be visible to shared user',
|
||||
project_uid: project.uid,
|
||||
});
|
||||
const noteInSharedProject = noteResponse.body;
|
||||
|
||||
// Shared user should see this note
|
||||
const response = await sharedUserAgent.get('/api/notes');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toBeDefined();
|
||||
|
||||
const foundNote = response.body.find(
|
||||
(n) => n.id === noteInSharedProject.id
|
||||
);
|
||||
expect(foundNote).toBeDefined();
|
||||
expect(foundNote.title).toBe('Note by owner in shared project');
|
||||
});
|
||||
|
||||
test('shared user should NOT see tasks in non-shared projects', async () => {
|
||||
// Create another project (not shared)
|
||||
const privateProjectResponse = await ownerAgent
|
||||
.post('/api/project')
|
||||
.send({
|
||||
name: 'Private Project',
|
||||
description: 'This should not be visible',
|
||||
});
|
||||
const privateProject = privateProjectResponse.body;
|
||||
|
||||
// Create task in private project
|
||||
const privateTaskResponse = await ownerAgent
|
||||
.post('/api/task')
|
||||
.send({
|
||||
name: 'Private task',
|
||||
project_id: privateProject.id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
const privateTask = privateTaskResponse.body;
|
||||
|
||||
// Shared user should NOT see this task
|
||||
const response = await sharedUserAgent.get('/api/tasks');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const foundTask = response.body.tasks.find(
|
||||
(t) => t.id === privateTask.id
|
||||
);
|
||||
expect(foundTask).toBeUndefined();
|
||||
});
|
||||
|
||||
test('shared user should see tasks due today in shared projects', async () => {
|
||||
// Create a task due today in shared project
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const taskDueTodayResponse = await ownerAgent
|
||||
.post('/api/task')
|
||||
.send({
|
||||
name: 'Task due today in shared project',
|
||||
project_id: project.id,
|
||||
due_date: today,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
const taskDueToday = taskDueTodayResponse.body;
|
||||
|
||||
// Fetch today's tasks as shared user
|
||||
const response = await sharedUserAgent.get('/api/tasks?type=today');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.tasks).toBeDefined();
|
||||
|
||||
// Check if the task appears in the main tasks or metrics
|
||||
const allTasks = [
|
||||
...response.body.tasks,
|
||||
...(response.body.metrics?.tasks_due_today || []),
|
||||
];
|
||||
|
||||
const foundTask = allTasks.find((t) => t.id === taskDueToday.id);
|
||||
expect(foundTask).toBeDefined();
|
||||
expect(foundTask.name).toBe('Task due today in shared project');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issue 2: Task and Note Creation in Shared Projects', () => {
|
||||
test('shared user with RW access can create tasks in shared project', async () => {
|
||||
const response = await sharedUserAgent.post('/api/task').send({
|
||||
name: 'Task created by shared user',
|
||||
project_id: project.id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(response.body.name).toBe('Task created by shared user');
|
||||
expect(response.body.project_id).toBe(project.id);
|
||||
});
|
||||
|
||||
test('shared user with RW access can create notes in shared project', async () => {
|
||||
const response = await sharedUserAgent.post('/api/note').send({
|
||||
title: 'Note created by shared user',
|
||||
content: 'Content of the note',
|
||||
project_uid: project.uid,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(response.body.title).toBe('Note created by shared user');
|
||||
});
|
||||
|
||||
test('shared user with RO access cannot create tasks', async () => {
|
||||
// Change permission to read-only
|
||||
await Permission.update(
|
||||
{ access_level: 'ro' },
|
||||
{
|
||||
where: {
|
||||
resource_uid: project.uid,
|
||||
user_id: sharedUser.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const response = await sharedUserAgent.post('/api/task').send({
|
||||
name: 'Task that should fail',
|
||||
project_id: project.id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
test('shared user with RO access cannot create notes', async () => {
|
||||
// Change permission to read-only
|
||||
await Permission.update(
|
||||
{ access_level: 'ro' },
|
||||
{
|
||||
where: {
|
||||
resource_uid: project.uid,
|
||||
user_id: sharedUser.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const response = await sharedUserAgent.post('/api/note').send({
|
||||
title: 'Note that should fail',
|
||||
content: 'Content',
|
||||
project_uid: project.uid,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task Timeline Access', () => {
|
||||
test('shared user can access task timeline in shared project', async () => {
|
||||
// Create a task in the shared project
|
||||
const taskResponse = await ownerAgent.post('/api/task').send({
|
||||
name: 'Task with timeline',
|
||||
project_id: project.id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
const taskInSharedProject = taskResponse.body;
|
||||
|
||||
// Shared user should be able to access the timeline
|
||||
const response = await sharedUserAgent.get(
|
||||
`/api/task/${taskInSharedProject.uid}/timeline`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
});
|
||||
|
||||
test('shared user cannot access task timeline in non-shared project', async () => {
|
||||
// Create a private project and task
|
||||
const privateProjectResponse = await ownerAgent
|
||||
.post('/api/project')
|
||||
.send({
|
||||
name: 'Private Project for Timeline',
|
||||
});
|
||||
const privateProject = privateProjectResponse.body;
|
||||
|
||||
const privateTaskResponse = await ownerAgent
|
||||
.post('/api/task')
|
||||
.send({
|
||||
name: 'Private task',
|
||||
project_id: privateProject.id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
const privateTask = privateTaskResponse.body;
|
||||
|
||||
// Shared user should NOT access this timeline
|
||||
const response = await sharedUserAgent.get(
|
||||
`/api/task/${privateTask.uid}/timeline`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
test('shared user can access completion time analytics', async () => {
|
||||
// Create a task in the shared project
|
||||
const taskResponse = await ownerAgent.post('/api/task').send({
|
||||
name: 'Task for completion analytics',
|
||||
project_id: project.id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
const taskInSharedProject = taskResponse.body;
|
||||
|
||||
const response = await sharedUserAgent.get(
|
||||
`/api/task/${taskInSharedProject.uid}/completion-time`
|
||||
);
|
||||
|
||||
// Should return 404 if not completed, or 200 with data if completed
|
||||
expect([200, 404]).toContain(response.status);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Owner sees their own tasks correctly', () => {
|
||||
test('owner should see all their tasks including those in shared projects', async () => {
|
||||
// Create a task in the shared project
|
||||
await ownerAgent.post('/api/task').send({
|
||||
name: 'Owner task in shared project',
|
||||
project_id: project.id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
|
||||
const response = await ownerAgent.get('/api/tasks');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.tasks).toBeDefined();
|
||||
expect(response.body.tasks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('owner should see tasks due today including shared project tasks', async () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
await ownerAgent.post('/api/task').send({
|
||||
name: 'Owner task due today',
|
||||
project_id: project.id,
|
||||
due_date: today,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
});
|
||||
|
||||
const response = await ownerAgent.get('/api/tasks?type=today');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.tasks).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -8,13 +8,13 @@ describe('Users Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com',
|
||||
email: `test_${Date.now()}@example.com`,
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
email: user.email,
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -466,13 +466,13 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
const uniqueDueDates = [...new Set(dueDates)];
|
||||
expect(uniqueDueDates.length).toBe(dueDates.length);
|
||||
|
||||
// Verify children have sequential due dates (within tolerance for floating point)
|
||||
// Verify children have sequential due dates (within tolerance for DST transitions)
|
||||
const sortedDueDates = dueDates.sort();
|
||||
for (let i = 1; i < sortedDueDates.length; i++) {
|
||||
const dayDiff =
|
||||
(sortedDueDates[i] - sortedDueDates[i - 1]) /
|
||||
(24 * 60 * 60 * 1000);
|
||||
expect(Math.abs(dayDiff - 1)).toBeLessThan(0.001); // Each task should be ~1 day apart
|
||||
expect(Math.abs(dayDiff - 1)).toBeLessThan(0.05); // Each task should be ~1 day apart (allowing for DST)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
20
package-lock.json
generated
20
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "tududi",
|
||||
"version": "v0.84.1-rc1",
|
||||
"version": "v0.84.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tududi",
|
||||
"version": "v0.84.1-rc1",
|
||||
"version": "v0.84.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.5",
|
||||
|
|
@ -89,6 +89,7 @@
|
|||
"sequelize-cli": "~6.6.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"supertest": "~7.1.1",
|
||||
"supertest-session": "^5.0.1",
|
||||
"swc-loader": "^0.2.6",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"ts-jest": "^29.0.0",
|
||||
|
|
@ -17934,6 +17935,21 @@
|
|||
"node": ">=14.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supertest-session": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/supertest-session/-/supertest-session-5.0.1.tgz",
|
||||
"integrity": "sha512-RpR8tGQZGreQsOCiW3YMSPKMwPlAB8lA0Jyat+8VUSJaYvLHTMqhMW6gooJ2htzjr3w/kgqJTQDnmuFenzA9JA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookiejar": "^2.1.2",
|
||||
"methods": "^1.1.2",
|
||||
"object-assign": "^4.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"supertest": ">= 3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@
|
|||
"sequelize-cli": "~6.6.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"supertest": "~7.1.1",
|
||||
"supertest-session": "^5.0.1",
|
||||
"swc-loader": "^0.2.6",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"ts-jest": "^29.0.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue