Initial commit in this repository

This commit is contained in:
Rapturate
2026-04-27 22:16:17 -04:00
commit 68e7058ca4
64 changed files with 20817 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
.env
/src/generated/prisma

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"bracketSpacing": true,
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"useTabs": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

53
docs/design/Roadmap Normal file
View File

@@ -0,0 +1,53 @@
# IDEA TRACKER API - DEVELOPMENT ROADMAP
# PHASE 1: MIDDLEWARE & VALIDATION
[x] Create authMiddleware.js - JWT verification and user authentication
[x] Create validateIdeas.js - Request validation for idea endpoints - Validate POST: name (3+ chars), description (required) - Validate PUT: same as POST, but both optional - Validate param id as positive integer
[x] Create validateProjects.js - Request validation for project endpoints - Validate POST: name (3+ chars), description (required) - Validate PUT: same as POST, but both optional - Validate param id as positive integer
[x] Create validateMaterials.js - Request validation for material endpoints - Validate POST: projectId, name, description, source, author, text (all required) - Validate DELETE: param id as positive integer
[x] Update/Review handleValidationErrors.js to ensure proper error responses
# PHASE 2: REPOSITORY LAYER (DATA ACCESS)
[x] Create ideasRepo.js - getAll(userId) - fetch all user ideas - getById(ideaId, userId) - fetch single idea with ownership check - create(userId, { name, description }) - update(ideaId, userId, { name, description }) - delete(ideaId, userId) - existsByName(userId, name) - check for duplicates
[x] Create projectsRepo.js - getAll(userId) - getById(projectId, userId) - create(userId, { name, description }) - update(projectId, userId, { name, description }) - delete(projectId, userId) - existsByName(userId, name)
[x] Create materialsRepo.js - getAll(userId) - all user materials - getByProjectId(projectId, userId) - materials for specific project - getById(materialId, userId) - single material with access check - create(userId, { projectId, name, description, source, author, text }) - delete(materialId, userId) - existsByName(userId, name)
# PHASE 3: SERVICE LAYER (BUSINESS LOGIC)
[x] Create ideasService.js - getAllIdeas(userId) - getIdeaById(ideaId, userId) - with 404 handling - createIdea(userId, { name, description }) - with duplicate check (409) - updateIdea(ideaId, userId, { name, description }) - with duplicate check - deleteIdea(ideaId, userId) - with 404 handling
[x] Create projectsService.js - Same methods as ideas service
[x] Create materialsService.js - getAllMaterials(userId) - getMaterialsByProject(projectId, userId) - createMaterial(userId, { projectId, name, description, source, author, text }) - deleteMaterial(materialId, userId) - Include project existence validation before creating material
# PHASE 4: CONTROLLER LAYER (REQUEST HANDLERS)
[x] Create ideasController.js - getAll(req, res) - GET /api/ideas - getById(req, res) - GET /api/ideas/:id - create(req, res) - POST /api/ideas - update(req, res) - PUT /api/ideas/:id - delete(req, res) - DELETE /api/ideas/:id
[x] Create projectsController.js - getAll(req, res) - GET /api/projects - getById(req, res) - GET /api/projects/:id - create(req, res) - POST /api/projects - update(req, res) - PUT /api/projects/:id - delete(req, res) - DELETE /api/projects/:id
[x] Create materialsController.js - getAll(req, res) - GET /api/materials - getByProject(req, res) - GET /api/materials/:projectId - create(req, res) - POST /api/materials - delete(req, res) - DELETE /api/materials/:id
# PHASE 5: ROUTES
[x] Create ideasRoutes.js - Route all endpoints with validation & auth middleware
[x] Create projectsRoutes.js - Route all endpoints with validation & auth middleware
[x] Create materialsRoutes.js - Route all endpoints with validation & auth middleware
# PHASE 6: UPDATE SERVER
[x] Update server.js - Replace /api/posts with /api/ideas - Add /api/projects routes - Add /api/materials routes
# PHASE 7: TESTING
[X] Test all endpoints with Swagger
[X] Verify JWT auth on protected routes
[x] Test 404, 409, 403 error cases
[X] Run seed script: npm run seed
[X] Verify Prettier formatting: npx prettier --write .
# PHASE 8: POLISH
[X] Write swagger for pagination
[X] Write code for POST project Files (within projects layers)
[X] Write code for DELETE project Files (within projects layers)
[X] Edit swagger to show an option for "upload a file to this project"
[X] create a path for downloading all files related to a project

BIN
docs/design/Roadmap.docx Normal file

Binary file not shown.

699
docs/openapi.yaml Normal file
View File

@@ -0,0 +1,699 @@
openapi: 3.1.0
info:
title: Idea Tracker API
version: 1.0.0
description: A REST API for managing project ideas, projects, materials, and files
servers:
- url: http://localhost:8080
description: Local development server
tags:
- name: Authentication
description: User authentication endpoints
- name: Ideas
description: Idea management endpoints
- name: Projects
description: Project management endpoints
- name: Materials
description: Project materials and resources
paths:
/api/auth/signup:
post:
summary: Create a new user account
tags:
- Authentication
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SignupRequest'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
'400':
description: Invalid input or validation error
'409':
description: Username already exists
/api/auth/login:
post:
summary: Login and receive JWT token
tags:
- Authentication
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Login successful
content:
application/json:
schema:
$ref: '#/components/schemas/LoginResponse'
'401':
description: Invalid credentials
/api/ideas:
get:
summary: Get all ideas for the authenticated user
tags:
- Ideas
security:
- BearerAuth: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Idea'
'401':
description: Unauthorized - Missing or invalid token
post:
summary: Create a new idea
tags:
- Ideas
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IdeaRequest'
responses:
'201':
description: Idea created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Idea'
'400':
description: Bad Request - Validation error
'401':
description: Unauthorized - Missing or invalid token
'409':
description: Conflict - Idea with that name already exists
/api/ideas/{id}:
put:
summary: Update an idea
tags:
- Ideas
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IdeaUpdateRequest'
responses:
'200':
description: Idea updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Idea'
'400':
description: Bad Request - Id not a positive integer, or no fields provided
'401':
description: Unauthorized - Missing or invalid token
'404':
description: Not Found - No idea with that id belongs to this user
'409':
description: Conflict - Idea with that name already exists
delete:
summary: Delete an idea
tags:
- Ideas
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'204':
description: Idea deleted successfully
'400':
description: Bad Request - Id not a positive integer
'401':
description: Unauthorized - Missing or invalid token
'404':
description: Not Found - No idea with that id belongs to this user
/api/projects:
get:
summary: Get all projects for the authenticated user
tags:
- Projects
security:
- BearerAuth: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Project'
'401':
description: Unauthorized - Missing or invalid token
post:
summary: Create a new project
tags:
- Projects
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectRequest'
responses:
'201':
description: Project created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Project'
'400':
description: Bad Request - Validation error
'401':
description: Unauthorized - Missing or invalid token
'409':
description: Conflict - Project with that name already exists
/api/projects/{id}:
get:
summary: Get a project by ID
tags:
- Projects
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Project'
'400':
description: Bad Request - Id not a positive integer
'401':
description: Unauthorized - Missing or invalid token
'404':
description: Not Found - No project with that id belongs to this user
put:
summary: Update a project
tags:
- Projects
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectUpdateRequest'
responses:
'200':
description: Project updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Project'
'400':
description: Bad Request - Id not a positive integer, or no fields provided
'401':
description: Unauthorized - Missing or invalid token
'404':
description: Not Found - No project with that id belongs to this user
'409':
description: Conflict - Project with that name already exists
delete:
summary: Delete a project
tags:
- Projects
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'204':
description: Project deleted successfully
'400':
description: Bad Request - Id not a positive integer
'401':
description: Unauthorized - Missing or invalid token
'404':
description: Not Found - No project with that id belongs to this user
/api/projects/{id}/files:
get:
summary: List all files related to a project
tags:
- Projects
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/File'
'400':
description: Bad Request - Id not a positive integer
'401':
description: Unauthorized - Missing or invalid token
'404':
description: Not Found - No project with that id belongs to this user
post:
summary: Upload a file (Max 10MB)
tags: [Projects]
security: [{ BearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/IdParam'
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
responses:
'201':
description: File uploaded
'400':
description: Bad Request - Id not a positive integer
'401':
description: Unauthorized - Missing or invalid token
'404':
description: Not Found - No project with that id belongs to this user
'413':
description: Payload Too Large - File size exceeds 10MB limit
/api/projects/files/{fileId}:
delete:
summary: Delete a specific project file
tags: [Projects]
security: [{ BearerAuth: [] }]
parameters:
- name: fileId
in: path
required: true
schema:
type: integer
responses:
'204':
description: File deleted
'404':
description: Not Found or Unauthorized
/api/projects/{id}/files/download:
get:
summary: Download all project files as a ZIP
tags: [Projects]
security: [{ BearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/IdParam'
responses:
'200':
description: A ZIP file containing all project documents
content:
application/zip:
schema:
type: string
format: binary
/api/materials:
get:
summary: Get all materials for the authenticated user
tags:
- Materials
security:
- BearerAuth: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Material'
'401':
description: Unauthorized - Missing or invalid token
post:
summary: Create a new material
tags:
- Materials
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MaterialRequest'
responses:
'201':
description: Material created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Material'
'400':
description: Bad Request - Validation error
'401':
description: Unauthorized - Missing or invalid token
'409':
description: Conflict - Material with that name already exists
/api/materials/project/{projectId}:
get:
summary: Get all materials for a specific project
tags:
- Materials
security:
- BearerAuth: []
parameters:
- name: projectId
in: path
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Material'
'400':
description: Bad Request - projectId not a positive integer
'401':
description: Unauthorized - Missing or invalid token
/api/materials/{id}:
delete:
summary: Delete a material by ID
tags:
- Materials
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'204':
description: Material deleted successfully
'400':
description: Bad Request - Id not a positive integer
'401':
description: Unauthorized - Missing or invalid token
'404':
description: Not Found - No material with that id belongs to this user
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
IdParam:
name: id
in: path
required: true
schema: { type: integer }
SearchQuery:
name: search
in: query
schema: { type: string }
SortBy:
name: sortBy
in: query
schema: { type: string, enum: [name, date_created, id] }
Limit:
name: limit
in: query
schema: { type: integer, default: 10 }
Offset:
name: offset
in: query
schema: { type: integer, default: 0 }
schemas:
SignupRequest:
type: object
required:
- username
- password
properties:
username:
type: string
example: new_user_1
password:
type: string
format: password
example: newpassword1234
LoginRequest:
type: object
required:
- username
- password
properties:
username:
type: string
example: new_user_1
password:
type: string
format: password
example: newpassword1234
UserResponse:
type: object
properties:
id:
type: integer
example: 10
username:
type: string
example: new_user_1
LoginResponse:
type: object
properties:
accessToken:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Idea:
type: object
properties:
id:
type: integer
readOnly: true
example: 1
name:
type: string
example: AI-Powered Task Manager
description:
type: string
example: A web app that uses AI to prioritize tasks intelligently
date_created:
type: string
format: date-time
readOnly: true
IdeaRequest:
type: object
required:
- name
- description
properties:
name:
type: string
example: AI-Powered Task Manager
description:
type: string
example: A web app that uses AI to prioritize tasks intelligently
IdeaUpdateRequest:
type: object
description: At least one field must be provided
properties:
name:
type: string
example: AI-Powered Task Manager
description:
type: string
example: A web app that uses AI to prioritize tasks intelligently
Project:
type: object
properties:
id:
type: integer
readOnly: true
example: 1
name:
type: string
example: Project 1
description:
type: string
example: Project 1 description
date_created:
type: string
format: date-time
readOnly: true
files:
type: array
readOnly: true
items:
$ref: '#/components/schemas/File'
ProjectRequest:
type: object
required:
- name
- description
properties:
name:
type: string
example: Project 1
description:
type: string
example: Project 1 description
ProjectUpdateRequest:
type: object
description: At least one field must be provided
properties:
name:
type: string
example: Project 1
description:
type: string
example: Project 1 description
Material:
type: object
properties:
id:
type: integer
readOnly: true
example: 1
projectId:
type: integer
example: 1
name:
type: string
example: Resource 1
description:
type: string
example: Resource 1 description
source:
type: string
example: Source 1
author:
type: string
example: Author 1
text:
type: string
example: Resource 1 text
MaterialRequest:
type: object
required:
- projectId
- name
- description
- source
- author
- text
properties:
projectId:
type: integer
example: 1
name:
type: string
example: Resource 1
description:
type: string
example: Resource 1 description
source:
type: string
example: Source 1
author:
type: string
example: Author 1
text:
type: string
example: Resource 1 text
File:
type: object
properties:
id: { type: integer }
projectId: { type: integer }
name: { type: string }
size: { type: integer }
mimeType: { type: string }

13
eslint.config.js Normal file
View File

@@ -0,0 +1,13 @@
import js from '@eslint/js';
import globals from 'globals';
import { defineConfig } from 'eslint/config';
export default defineConfig([
{
files: ['**/*.{js,mjs,cjs}'],
plugins: { js },
extends: ['js/recommended'],
rules: { 'no-unused-vars': 'none' },
},
{ files: ['**/*.{js,mjs,cjs}'], languageOptions: { globals: globals.node } },
]);

4597
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "Idea_Tracker",
"version": "1.0.0",
"description": "A simple idea tracking API built with Express and Prisma",
"main": "index.js",
"scripts": {
"dev": "node --env-file=.env --watch src/server.js",
"seed": "node prisma/seed.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"devDependencies": {
"@eslint/js": "^9.29.0",
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"globals": "^16.2.0",
"prettier": "^3.6.1",
"prisma": "^7.8.0"
},
"dependencies": {
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.8.0",
"archiver": "^7.0.1",
"bcrypt": "^6.0.0",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-validator": "^7.2.1",
"js-yaml": "^4.1.1",
"jsonwebtoken": "^9.0.3",
"morgan": "^1.10.1",
"multer": "^2.1.1",
"swagger-ui-express": "^5.0.1"
}
}

14
prisma.config.ts Normal file
View File

@@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: process.env['DATABASE_URL'],
},
});

View File

@@ -0,0 +1,80 @@
-- CreateTable
CREATE TABLE "users" (
"id" SERIAL NOT NULL,
"username" TEXT NOT NULL,
"password" TEXT NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ideas" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"date_created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"user_id" INTEGER NOT NULL,
CONSTRAINT "ideas_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "projects" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"date_created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"user_id" INTEGER NOT NULL,
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "materials" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"source" TEXT NOT NULL,
"author" TEXT NOT NULL,
"text" TEXT NOT NULL,
"project_id" INTEGER NOT NULL,
CONSTRAINT "materials_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "files" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"file" BYTEA NOT NULL,
"project_id" INTEGER NOT NULL,
CONSTRAINT "files_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-- CreateIndex
CREATE INDEX "ideas_user_id_idx" ON "ideas"("user_id");
-- CreateIndex
CREATE INDEX "projects_user_id_idx" ON "projects"("user_id");
-- CreateIndex
CREATE INDEX "materials_project_id_idx" ON "materials"("project_id");
-- CreateIndex
CREATE INDEX "files_project_id_idx" ON "files"("project_id");
-- AddForeignKey
ALTER TABLE "ideas" ADD CONSTRAINT "ideas_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "materials" ADD CONSTRAINT "materials_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "files" ADD CONSTRAINT "files_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,10 @@
/*
Warnings:
- Added the required column `mimeType` to the `files` table without a default value. This is not possible if the table is not empty.
- Added the required column `size` to the `files` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "files" ADD COLUMN "mimeType" TEXT NOT NULL,
ADD COLUMN "size" INTEGER NOT NULL;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

71
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,71 @@
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma.js"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
username String @unique
password String
ideas Idea[]
projects Project[]
@@map("users")
}
model Idea {
id Int @id @default(autoincrement())
name String
description String
date_created DateTime @default(now()) @map("date_created")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int @map("user_id")
@@index([userId])
@@map("ideas")
}
model Project {
id Int @id @default(autoincrement())
name String
description String
date_created DateTime @default(now()) @map("date_created")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int @map("user_id")
materials Material[]
files File[]
@@index([userId])
@@map("projects")
}
model Material {
id Int @id @default(autoincrement())
name String
description String
source String
author String
text String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId Int @map("project_id")
@@index([projectId])
@@map("materials")
}
model File {
id Int @id @default(autoincrement())
name String
file Bytes
size Int
mimeType String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId Int @map("project_id")
@@index([projectId])
@@map("files")
}

102
prisma/seed.js Normal file
View File

@@ -0,0 +1,102 @@
import bcrypt from 'bcrypt';
import 'dotenv/config';
import prisma from '../src/config/db.js';
try {
await prisma.$queryRaw`TRUNCATE users, ideas, projects, materials, files RESTART IDENTITY CASCADE;`;
// Create users
const usersData = [
{ username: 'alice_dev', password: 'alice1234' },
{ username: 'bob_maker', password: 'bob1234' },
{ username: 'charlie_creator', password: 'charlie1234' },
];
const users = [];
for (const userData of usersData) {
const hashedPassword = await bcrypt.hash(userData.password, 10);
const user = await prisma.user.create({
data: {
username: userData.username,
password: hashedPassword,
},
});
users.push(user);
}
// Create ideas for each user
for (const user of users) {
await prisma.idea.createMany({
data: [
{
name: 'AI-Powered Task Manager',
description:
'A web app that uses AI to prioritize tasks intelligently based on user patterns.',
userId: user.id,
},
{
name: 'Community Recipe Hub',
description:
'A platform where users can share, rate, and discover recipes from around the world.',
userId: user.id,
},
{
name: 'Real-time Collaboration Tool',
description:
'A tool for teams to brainstorm and collaborate in real-time with visual whiteboarding.',
userId: user.id,
},
],
});
}
// Create projects with materials for each user
for (const user of users) {
const project = await prisma.project.create({
data: {
name: `${user.username}'s Innovation Lab`,
description: `Main project workspace for ${user.username} to develop and prototype ideas.`,
userId: user.id,
},
});
// Add materials to the project
await prisma.material.createMany({
data: [
{
name: 'Research Paper on UX Design',
description: 'Key findings on modern UI/UX best practices.',
source: 'Nielsen Norman Group',
author: 'Don Norman',
text: 'User experience encompasses all aspects of the end-users interaction with the company, its services, and its products.',
projectId: project.id,
},
{
name: 'API Documentation Reference',
description: 'Technical specifications for third-party integrations.',
source: 'OpenAI API Docs',
author: 'OpenAI Team',
text: 'The API provides access to state-of-the-art language models for various NLP tasks.',
projectId: project.id,
},
{
name: 'Project Budget Template',
description: 'Financial planning and resource allocation framework.',
source: 'Internal Resources',
author: 'Finance Department',
text: 'Ensure all project expenses are tracked and approved according to company policy.',
projectId: project.id,
},
],
});
}
console.log('Seed completed successfully!');
} catch (error) {
console.error('Seed failed:', error);
} finally {
await prisma.$disconnect();
}

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

7
src/config/db.js Normal file
View File

@@ -0,0 +1,7 @@
import { PrismaClient } from '../generated/prisma.js/client.js';
import { PrismaPg } from '@prisma/adapter-pg';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
export default prisma;

View File

@@ -0,0 +1,13 @@
import { signUp, logIn } from '../services/authService.js';
export async function signUpHandler(req, res) {
const { username, password } = req.body;
const newUser = await signUp(username, password);
res.status(201).json(newUser);
}
export async function logInHandler(req, res) {
const { username, password } = req.body;
const accessToken = await logIn(username, password);
res.status(200).json({ accessToken });
}

View File

@@ -0,0 +1,43 @@
import {
getAllIdeas,
createIdea,
updateIdea,
deleteIdea,
} from '../services/ideasService.js';
export async function getAllIdeasHandler(req, res) {
const {
search = '',
sortBy = 'date_created',
order = 'desc',
offset = '0',
limit = '10',
} = req.query;
const options = {
search,
sortBy,
order,
offset: parseInt(offset),
limit: parseInt(limit),
};
const ideas = await getAllIdeas(req.user.id, options);
res.status(200).json(ideas);
}
export async function createIdeaHandler(req, res) {
const { name, description } = req.body;
const newIdea = await createIdea(req.user.id, { name, description });
res.status(201).json(newIdea);
}
export async function updateIdeaHandler(req, res) {
const id = parseInt(req.params.id);
const { name, description } = req.body;
const updatedIdea = await updateIdea(id, req.user.id, { name, description });
res.status(200).json(updatedIdea);
}
export async function deleteIdeaHandler(req, res) {
const id = parseInt(req.params.id);
await deleteIdea(id, req.user.id);
res.status(204).send();
}

View File

@@ -0,0 +1,79 @@
import {
getAllMaterials,
getMaterialsByProject,
getMaterialById,
createMaterial,
deleteMaterial,
} from '../services/materialsService.js';
export async function getAllMaterialsHandler(req, res) {
const {
search = '',
sortBy = 'id',
order = 'desc',
offset = '0',
limit = '10',
} = req.query;
const options = {
search,
sortBy,
order,
offset: parseInt(offset),
limit: parseInt(limit),
};
const materials = await getAllMaterials(req.user.id, options);
res.status(200).json(materials);
}
export async function getMaterialsByProjectHandler(req, res) {
const projectId = parseInt(req.params.projectId);
const {
search = '',
sortBy = 'id',
order = 'desc',
offset = '0',
limit = '10',
} = req.query;
const options = {
search,
sortBy,
order,
offset: parseInt(offset),
limit: parseInt(limit),
};
const materials = await getMaterialsByProject(
projectId,
req.user.id,
options
);
res.status(200).json(materials);
}
export async function getMaterialByIdHandler(req, res) {
const id = parseInt(req.params.id);
const material = await getMaterialById(id, req.user.id);
res.status(200).json(material);
}
export async function createMaterialHandler(req, res) {
const { projectId, name, description, source, author, text } = req.body;
const newMaterial = await createMaterial(req.user.id, {
projectId,
name,
description,
source,
author,
text,
});
res.status(201).json(newMaterial);
}
export async function deleteMaterialHandler(req, res) {
const id = parseInt(req.params.id);
await deleteMaterial(id, req.user.id);
res.status(204).send();
}

View File

@@ -0,0 +1,159 @@
import {
getAllProjects,
getProjectById,
createProject,
updateProject,
deleteProject,
getProjectFilesById,
addProjectFile,
deleteProjectFile,
getProjectFilesForDownload,
} from '../services/projectsService.js';
import archiver from 'archiver';
import { Buffer } from 'node:buffer';
export async function getAllProjectsHandler(req, res) {
const {
search = '',
sortBy = 'date_created',
order = 'desc',
offset = '0',
limit = '10',
} = req.query;
const options = {
search,
sortBy,
order,
offset: parseInt(offset),
limit: parseInt(limit),
};
const projects = await getAllProjects(req.user.id, options);
res.status(200).json(projects);
}
export async function getProjectByIdHandler(req, res) {
const id = parseInt(req.params.id);
const project = await getProjectById(id, req.user.id);
res.status(200).json(project);
}
export async function createProjectHandler(req, res) {
const { name, description } = req.body;
const newProject = await createProject(req.user.id, { name, description });
res.status(201).json(newProject);
}
export async function updateProjectHandler(req, res) {
const id = parseInt(req.params.id);
const { name, description } = req.body;
const updatedProject = await updateProject(id, req.user.id, {
name,
description,
});
res.status(200).json(updatedProject);
}
export async function deleteProjectHandler(req, res) {
const id = parseInt(req.params.id);
await deleteProject(id, req.user.id);
res.status(204).send();
}
export async function getProjectFilesHandler(req, res) {
const id = parseInt(req.params.id);
const files = await getProjectFilesById(id, req.user.id);
res.status(200).json(files);
}
export async function addProjectFileHandler(req, res) {
let id;
let fileData;
try {
id = parseInt(req.params.id);
if (!req.file) {
return res.status(400).json({ error: 'No file provided' });
}
fileData = {
name: req.file.originalname,
file: req.file.buffer,
size: req.file.size,
mimeType: req.file.mimetype
};
} catch (error) {
return res.status(400).json({ error: 'Invalid file data' });
}
try {
const newFile = await addProjectFile(id, req.user.id, fileData);
if (!newFile) {
return res.status(404).json({ error: 'Project file error' });
}
const { file, ...metadata } = newFile;
res.status(201).json(metadata);
} catch (error) {
res.status(error.status || 500).json({ error: error.message });
}
}
export async function deleteProjectFileHandler(req, res) {
try {
const fileId = parseInt(req.params.id);
const deletedFile = await deleteProjectFile(fileId, req.user.id);
if (!deletedFile) {
return res.status(404).json({ error: 'File not found or unauthorized' });
}
res.status(200).json({ message: 'File deleted successfully' });
} catch (error) {
res.status(400).json({ error: 'Invalid file ID' });
}
}
export async function downloadProjectFilesHandler(req, res) {
try {
const fileId = parseInt(req.params.id);
const userId = req.user.id;
const files = await getProjectFilesForDownload(fileId, userId);
if (!files || files.length === 0) {
return res.status(404).json({ error: 'No files found for this project' });
}
res.set({
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="project_${fileId}_files.zip"`
});
const archive = archiver('zip', { zlib: { level: 9 } });
archive.pipe(res);
files.forEach(f => {
const binaryData = f.file;
if (binaryData) {
const bufferData = Buffer.from(binaryData);
archive.append(bufferData, { name: f.name });
}
else {
console.error(`ERROR: No binary data found for ${f.name}. Object was:`, f);
}
});
await archive.finalize();
} catch (error) {
if (!res.headersSent) {
res.status(error.status || 500).json({ error: error.message });
}
}
}

1
src/generated/prisma.js/client.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from "./index"

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }

1
src/generated/prisma.js/default.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from "./index"

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('#main-entry-point') }

1
src/generated/prisma.js/edge.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from "./default"

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,212 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
DbNull,
JsonNull,
AnyNull,
NullTypes,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('./runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 7.8.0
* Query Engine version: 3c6e192761c0362d496ed980de936e2f3cebcd3a
*/
Prisma.prismaVersion = {
client: "7.8.0",
engine: "3c6e192761c0362d496ed980de936e2f3cebcd3a"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = DbNull
Prisma.JsonNull = JsonNull
Prisma.AnyNull = AnyNull
Prisma.NullTypes = NullTypes
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
username: 'username',
password: 'password'
};
exports.Prisma.IdeaScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description',
date_created: 'date_created',
userId: 'userId'
};
exports.Prisma.ProjectScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description',
date_created: 'date_created',
userId: 'userId'
};
exports.Prisma.MaterialScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description',
source: 'source',
author: 'author',
text: 'text',
projectId: 'projectId'
};
exports.Prisma.FileScalarFieldEnum = {
id: 'id',
name: 'name',
file: 'file',
size: 'size',
mimeType: 'mimeType',
projectId: 'projectId'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.ModelName = {
User: 'User',
Idea: 'Idea',
Project: 'Project',
Material: 'Material',
File: 'File'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

8890
src/generated/prisma.js/index.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,144 @@
{
"name": "prisma-client-8a7d0bd0282d327d88c7d480c03ad5f66befdf72c92e9b6d7469f459b777fd67",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
"exports": {
"./client": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
"node": {
"require": "./runtime/client.js",
"default": "./runtime/client.js"
},
"require": "./runtime/client.js",
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/index-browser": {
"types": "./runtime/index-browser.d.ts",
"require": "./runtime/index-browser.js",
"import": "./runtime/index-browser.mjs",
"default": "./runtime/index-browser.mjs"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "7.8.0",
"sideEffects": false,
"dependencies": {
"@prisma/client-runtime-utils": "7.8.0"
},
"imports": {
"#wasm-compiler-loader": {
"edge-light": "./wasm-edge-light-loader.mjs",
"workerd": "./wasm-worker-loader.mjs",
"worker": "./wasm-worker-loader.mjs",
"default": "./wasm-worker-loader.mjs"
},
"#main-entry-point": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
}
}
}

View File

@@ -0,0 +1,2 @@
"use strict";var h=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var j=Object.prototype.hasOwnProperty;var D=(e,t)=>{for(var n in t)h(e,n,{get:t[n],enumerable:!0})},O=(e,t,n,_)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of M(t))!j.call(e,r)&&r!==n&&h(e,r,{get:()=>t[r],enumerable:!(_=T(t,r))||_.enumerable});return e};var B=e=>O(h({},"__esModule",{value:!0}),e);var xe={};D(xe,{QueryCompiler:()=>F,__wbg_Error_e83987f665cf5504:()=>q,__wbg_Number_bb48ca12f395cd08:()=>C,__wbg_String_8f0eb39a4a4c2f66:()=>k,__wbg___wbindgen_boolean_get_6d5a1ee65bab5f68:()=>W,__wbg___wbindgen_debug_string_df47ffb5e35e6763:()=>V,__wbg___wbindgen_in_bb933bd9e1b3bc0f:()=>z,__wbg___wbindgen_is_object_c818261d21f283a4:()=>L,__wbg___wbindgen_is_string_fbb76cb2940daafd:()=>P,__wbg___wbindgen_is_undefined_2d472862bd29a478:()=>Q,__wbg___wbindgen_jsval_loose_eq_b664b38a2f582147:()=>Y,__wbg___wbindgen_number_get_a20bf9b85341449d:()=>G,__wbg___wbindgen_string_get_e4f06c90489ad01b:()=>J,__wbg___wbindgen_throw_b855445ff6a94295:()=>X,__wbg_entries_e171b586f8f6bdbf:()=>H,__wbg_getTime_14776bfb48a1bff9:()=>K,__wbg_get_7bed016f185add81:()=>Z,__wbg_get_with_ref_key_1dc361bd10053bfe:()=>v,__wbg_instanceof_ArrayBuffer_70beb1189ca63b38:()=>ee,__wbg_instanceof_Uint8Array_20c8e73002f7af98:()=>te,__wbg_isSafeInteger_d216eda7911dde36:()=>ne,__wbg_length_69bca3cb64fc8748:()=>re,__wbg_length_cdd215e10d9dd507:()=>_e,__wbg_new_0_f9740686d739025c:()=>oe,__wbg_new_1acc0b6eea89d040:()=>ce,__wbg_new_5a79be3ab53b8aa5:()=>ie,__wbg_new_68651c719dcda04e:()=>se,__wbg_new_e17d9f43105b08be:()=>ue,__wbg_prototypesetcall_2a6620b6922694b2:()=>fe,__wbg_set_3f1d0b984ed272ed:()=>be,__wbg_set_907fb406c34a251d:()=>de,__wbg_set_c213c871859d6500:()=>ae,__wbg_set_message_82ae475bb413aa5c:()=>ge,__wbg_set_wasm:()=>N,__wbindgen_cast_2241b6af4c4b2941:()=>le,__wbindgen_cast_4625c577ab2ec9ee:()=>we,__wbindgen_cast_9ae0607507abb057:()=>pe,__wbindgen_cast_d6cd19b81560fd6e:()=>ye,__wbindgen_init_externref_table:()=>me});module.exports=B(xe);var A=()=>{};A.prototype=A;let o;function N(e){o=e}let p=null;function a(){return(p===null||p.byteLength===0)&&(p=new Uint8Array(o.memory.buffer)),p}let y=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});y.decode();const U=2146435072;let S=0;function R(e,t){return S+=t,S>=U&&(y=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),y.decode(),S=t),y.decode(a().subarray(e,e+t))}function m(e,t){return e=e>>>0,R(e,t)}let f=0;const g=new TextEncoder;"encodeInto"in g||(g.encodeInto=function(e,t){const n=g.encode(e);return t.set(n),{read:e.length,written:n.length}});function l(e,t,n){if(n===void 0){const i=g.encode(e),d=t(i.length,1)>>>0;return a().subarray(d,d+i.length).set(i),f=i.length,d}let _=e.length,r=t(_,1)>>>0;const s=a();let c=0;for(;c<_;c++){const i=e.charCodeAt(c);if(i>127)break;s[r+c]=i}if(c!==_){c!==0&&(e=e.slice(c)),r=n(r,_,_=c+e.length*3,1)>>>0;const i=a().subarray(r+c,r+_),d=g.encodeInto(e,i);c+=d.written,r=n(r,_,c,1)>>>0}return f=c,r}let b=null;function u(){return(b===null||b.buffer.detached===!0||b.buffer.detached===void 0&&b.buffer!==o.memory.buffer)&&(b=new DataView(o.memory.buffer)),b}function x(e){return e==null}function I(e){const t=typeof e;if(t=="number"||t=="boolean"||e==null)return`${e}`;if(t=="string")return`"${e}"`;if(t=="symbol"){const r=e.description;return r==null?"Symbol":`Symbol(${r})`}if(t=="function"){const r=e.name;return typeof r=="string"&&r.length>0?`Function(${r})`:"Function"}if(Array.isArray(e)){const r=e.length;let s="[";r>0&&(s+=I(e[0]));for(let c=1;c<r;c++)s+=", "+I(e[c]);return s+="]",s}const n=/\[object ([^\]]+)\]/.exec(toString.call(e));let _;if(n&&n.length>1)_=n[1];else return toString.call(e);if(_=="Object")try{return"Object("+JSON.stringify(e)+")"}catch{return"Object"}return e instanceof Error?`${e.name}: ${e.message}
${e.stack}`:_}function $(e,t){return e=e>>>0,a().subarray(e/1,e/1+t)}function w(e){const t=o.__wbindgen_externrefs.get(e);return o.__externref_table_dealloc(e),t}const E=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(e=>o.__wbg_querycompiler_free(e>>>0,1));class F{__destroy_into_raw(){const t=this.__wbg_ptr;return this.__wbg_ptr=0,E.unregister(this),t}free(){const t=this.__destroy_into_raw();o.__wbg_querycompiler_free(t,0)}compileBatch(t){const n=l(t,o.__wbindgen_malloc,o.__wbindgen_realloc),_=f,r=o.querycompiler_compileBatch(this.__wbg_ptr,n,_);if(r[2])throw w(r[1]);return w(r[0])}constructor(t){const n=o.querycompiler_new(t);if(n[2])throw w(n[1]);return this.__wbg_ptr=n[0]>>>0,E.register(this,this.__wbg_ptr,this),this}compile(t){const n=l(t,o.__wbindgen_malloc,o.__wbindgen_realloc),_=f,r=o.querycompiler_compile(this.__wbg_ptr,n,_);if(r[2])throw w(r[1]);return w(r[0])}}Symbol.dispose&&(F.prototype[Symbol.dispose]=F.prototype.free);function q(e,t){return Error(m(e,t))}function C(e){return Number(e)}function k(e,t){const n=String(t),_=l(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=f;u().setInt32(e+4*1,r,!0),u().setInt32(e+4*0,_,!0)}function W(e){const t=e,n=typeof t=="boolean"?t:void 0;return x(n)?16777215:n?1:0}function V(e,t){const n=I(t),_=l(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=f;u().setInt32(e+4*1,r,!0),u().setInt32(e+4*0,_,!0)}function z(e,t){return e in t}function L(e){const t=e;return typeof t=="object"&&t!==null}function P(e){return typeof e=="string"}function Q(e){return e===void 0}function Y(e,t){return e==t}function G(e,t){const n=t,_=typeof n=="number"?n:void 0;u().setFloat64(e+8*1,x(_)?0:_,!0),u().setInt32(e+4*0,!x(_),!0)}function J(e,t){const n=t,_=typeof n=="string"?n:void 0;var r=x(_)?0:l(_,o.__wbindgen_malloc,o.__wbindgen_realloc),s=f;u().setInt32(e+4*1,s,!0),u().setInt32(e+4*0,r,!0)}function X(e,t){throw new Error(m(e,t))}function H(e){return Object.entries(e)}function K(e){return e.getTime()}function Z(e,t){return e[t>>>0]}function v(e,t){return e[t]}function ee(e){let t;try{t=e instanceof ArrayBuffer}catch{t=!1}return t}function te(e){let t;try{t=e instanceof Uint8Array}catch{t=!1}return t}function ne(e){return Number.isSafeInteger(e)}function re(e){return e.length}function _e(e){return e.length}function oe(){return new Date}function ce(){return new Object}function ie(e){return new Uint8Array(e)}function se(){return new Map}function ue(){return new Array}function fe(e,t,n){Uint8Array.prototype.set.call($(e,t),n)}function be(e,t,n){e[t]=n}function de(e,t,n){return e.set(t,n)}function ae(e,t,n){e[t>>>0]=n}function ge(e,t){global.PRISMA_WASM_PANIC_REGISTRY.set_message(m(e,t))}function le(e,t){return m(e,t)}function we(e){return BigInt.asUintN(64,e)}function pe(e){return e}function ye(e){return e}function me(){const e=o.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}0&&(module.exports={QueryCompiler,__wbg_Error_e83987f665cf5504,__wbg_Number_bb48ca12f395cd08,__wbg_String_8f0eb39a4a4c2f66,__wbg___wbindgen_boolean_get_6d5a1ee65bab5f68,__wbg___wbindgen_debug_string_df47ffb5e35e6763,__wbg___wbindgen_in_bb933bd9e1b3bc0f,__wbg___wbindgen_is_object_c818261d21f283a4,__wbg___wbindgen_is_string_fbb76cb2940daafd,__wbg___wbindgen_is_undefined_2d472862bd29a478,__wbg___wbindgen_jsval_loose_eq_b664b38a2f582147,__wbg___wbindgen_number_get_a20bf9b85341449d,__wbg___wbindgen_string_get_e4f06c90489ad01b,__wbg___wbindgen_throw_b855445ff6a94295,__wbg_entries_e171b586f8f6bdbf,__wbg_getTime_14776bfb48a1bff9,__wbg_get_7bed016f185add81,__wbg_get_with_ref_key_1dc361bd10053bfe,__wbg_instanceof_ArrayBuffer_70beb1189ca63b38,__wbg_instanceof_Uint8Array_20c8e73002f7af98,__wbg_isSafeInteger_d216eda7911dde36,__wbg_length_69bca3cb64fc8748,__wbg_length_cdd215e10d9dd507,__wbg_new_0_f9740686d739025c,__wbg_new_1acc0b6eea89d040,__wbg_new_5a79be3ab53b8aa5,__wbg_new_68651c719dcda04e,__wbg_new_e17d9f43105b08be,__wbg_prototypesetcall_2a6620b6922694b2,__wbg_set_3f1d0b984ed272ed,__wbg_set_907fb406c34a251d,__wbg_set_c213c871859d6500,__wbg_set_message_82ae475bb413aa5c,__wbg_set_wasm,__wbindgen_cast_2241b6af4c4b2941,__wbindgen_cast_4625c577ab2ec9ee,__wbindgen_cast_9ae0607507abb057,__wbindgen_cast_d6cd19b81560fd6e,__wbindgen_init_externref_table});

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,90 @@
import { AnyNull } from '@prisma/client-runtime-utils';
import { DbNull } from '@prisma/client-runtime-utils';
import { Decimal } from '@prisma/client-runtime-utils';
import { isAnyNull } from '@prisma/client-runtime-utils';
import { isDbNull } from '@prisma/client-runtime-utils';
import { isJsonNull } from '@prisma/client-runtime-utils';
import { isObjectEnumValue } from '@prisma/client-runtime-utils';
import { JsonNull } from '@prisma/client-runtime-utils';
import { NullTypes } from '@prisma/client-runtime-utils';
export { AnyNull }
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
types: {
operations: {
[K in F]: {
args: any;
};
};
};
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
export { DbNull }
export { Decimal }
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
} : W) : never) | (A extends Narrowable ? A : never);
export declare function getRuntime(): GetRuntimeOutput;
declare type GetRuntimeOutput = {
id: RuntimeName;
prettyName: string;
isEdge: boolean;
};
export { isAnyNull }
export { isDbNull }
export { isJsonNull }
export { isObjectEnumValue }
export { JsonNull }
/**
* Generates more strict variant of an enum which, unlike regular enum,
* throws on non-existing property access. This can be useful in following situations:
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
* - enum values are generated dynamically from DMMF.
*
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
* will result in `undefined` value being used, which will be accepted. Using strict enum
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
*
* Note: if you need to check for existence of a value in the enum you can still use either
* `in` operator or `hasOwnProperty` function.
*
* @param definition
* @returns
*/
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
declare type Narrowable = string | number | bigint | boolean | [];
export { NullTypes }
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
declare namespace Public {
export {
validator
}
}
export { Public }
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
export { }

View File

@@ -0,0 +1,6 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
"use strict";var s=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var f=Object.prototype.hasOwnProperty;var a=(e,t)=>{for(var n in t)s(e,n,{get:t[n],enumerable:!0})},y=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of p(t))!f.call(e,i)&&i!==n&&s(e,i,{get:()=>t[i],enumerable:!(r=g(t,i))||r.enumerable});return e};var x=e=>y(s({},"__esModule",{value:!0}),e);var M={};a(M,{AnyNull:()=>o.AnyNull,DbNull:()=>o.DbNull,Decimal:()=>m.Decimal,JsonNull:()=>o.JsonNull,NullTypes:()=>o.NullTypes,Public:()=>l,getRuntime:()=>c,isAnyNull:()=>o.isAnyNull,isDbNull:()=>o.isDbNull,isJsonNull:()=>o.isJsonNull,isObjectEnumValue:()=>o.isObjectEnumValue,makeStrictEnum:()=>u});module.exports=x(M);var l={};a(l,{validator:()=>d});function d(...e){return t=>t}var b=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function u(e){return new Proxy(e,{get(t,n){if(n in t)return t[n];if(!b.has(n))throw new TypeError("Invalid enum value: ".concat(String(n)))}})}var N=()=>{var e,t;return((t=(e=globalThis.process)==null?void 0:e.release)==null?void 0:t.name)==="node"},E=()=>{var e,t;return!!globalThis.Bun||!!((t=(e=globalThis.process)==null?void 0:e.versions)!=null&&t.bun)},S=()=>!!globalThis.Deno,R=()=>typeof globalThis.Netlify=="object",h=()=>typeof globalThis.EdgeRuntime=="object",C=()=>{var e;return((e=globalThis.navigator)==null?void 0:e.userAgent)==="Cloudflare-Workers"};function k(){var n;return(n=[[R,"netlify"],[h,"edge-light"],[C,"workerd"],[S,"deno"],[E,"bun"],[N,"node"]].flatMap(r=>r[0]()?[r[1]]:[]).at(0))!=null?n:""}var O={node:"Node.js",workerd:"Cloudflare Workers",deno:"Deno and Deno Deploy",netlify:"Netlify Edge Functions","edge-light":"Edge Runtime (Vercel Edge Functions, Vercel Edge Middleware, Next.js (Pages Router) Edge API Routes, Next.js (App Router) Edge Route Handlers or Next.js Middleware)"};function c(){let e=k();return{id:e,prettyName:O[e]||e,isEdge:["workerd","deno","netlify","edge-light"].includes(e)}}var o=require("@prisma/client-runtime-utils"),m=require("@prisma/client-runtime-utils");0&&(module.exports={AnyNull,DbNull,Decimal,JsonNull,NullTypes,Public,getRuntime,isAnyNull,isDbNull,isJsonNull,isObjectEnumValue,makeStrictEnum});
//# sourceMappingURL=index-browser.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,71 @@
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma.js"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
username String @unique
password String
ideas Idea[]
projects Project[]
@@map("users")
}
model Idea {
id Int @id @default(autoincrement())
name String
description String
date_created DateTime @default(now()) @map("date_created")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int @map("user_id")
@@index([userId])
@@map("ideas")
}
model Project {
id Int @id @default(autoincrement())
name String
description String
date_created DateTime @default(now()) @map("date_created")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int @map("user_id")
materials Material[]
files File[]
@@index([userId])
@@map("projects")
}
model Material {
id Int @id @default(autoincrement())
name String
description String
source String
author String
text String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId Int @map("project_id")
@@index([projectId])
@@map("materials")
}
model File {
id Int @id @default(autoincrement())
name String
file Bytes
size Int
mimeType String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId Int @map("project_id")
@@index([projectId])
@@map("files")
}

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_compiler_fast_bg.wasm?module')

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_compiler_fast_bg.wasm')

View File

@@ -0,0 +1,38 @@
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) throw new Error('JWT_SECRET environment variable is not set');
export function authenticate(req, res, next) {
if (process.env.NODE_ENV === 'development') {
console.log('Auth middleware reached!');
}
const authHeader = req.headers.authorization;
if (process.env.NODE_ENV === 'development') {
console.log('Header found:', authHeader);
}
const err = new Error('Not authenticated. Please provide a valid token.');
err.status = 401;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next(err);
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, JWT_SECRET);
if (process.env.NODE_ENV === 'development') {
console.log('Decoded JWT Payload:', payload);
}
req.user = { id: payload.id, role: payload.role };
next();
} catch (error) {
return next(err);
}
}

View File

@@ -0,0 +1,17 @@
import { validationResult } from 'express-validator';
export function handleValidationErrors(req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
if (process.env.NODE_ENV === 'development') {
console.log('Validation errors:', errors.array());
}
return res.status(400).json({
errors: errors.array().map((err) => ({
field: err.path,
message: err.msg,
})),
});
}
next();
}

View File

@@ -0,0 +1,34 @@
import { body } from 'express-validator';
import { handleValidationErrors } from './handleValidationErrors.js';
export const validateSignUp = [
body('username')
.exists({ checkFalsy: true })
.withMessage('Username is required')
.bail()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage('Username must be at least 3 characters'),
body('password')
.exists({ checkFalsy: true })
.withMessage('Password is required')
.bail()
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters'),
handleValidationErrors,
];
export const validateLogIn = [
body('username')
.exists({ checkFalsy: true })
.withMessage('Username is required'),
body('password')
.exists({ checkFalsy: true })
.withMessage('Password is required'),
handleValidationErrors,
];

View File

@@ -0,0 +1,93 @@
import { param, query, body, oneOf } from 'express-validator';
import { handleValidationErrors } from './handleValidationErrors.js';
export const validateGetAllQuery = [
query('search')
.optional()
.trim()
.isString()
.withMessage('Search must be a string'),
query('sortBy')
.optional()
.isIn(['name', 'date_created'])
.withMessage('sortBy must be one of: name, date_created'),
query('order')
.optional()
.isIn(['asc', 'desc'])
.withMessage('order must be asc or desc'),
query('offset')
.optional()
.isInt({ min: 0 })
.withMessage('offset must be a non-negative integer'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('limit must be an integer between 1 and 100'),
handleValidationErrors,
];
export const validateId = [
param('id')
.trim()
.escape()
.isInt({ min: 1 })
.withMessage('Id must be a positive integer'),
handleValidationErrors,
];
export const validateCreateIdea = [
body('name')
.exists({ checkFalsy: true })
.withMessage('Name is required')
.bail()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage('Name must be at least 3 characters'),
body('description')
.exists({ checkFalsy: true })
.withMessage('Description is required')
.bail()
.trim()
.escape()
.isString()
.withMessage('Description must be a string'),
handleValidationErrors,
];
export const validateUpdateIdea = [
oneOf(
[
body('name').exists({ checkFalsy: true }),
body('description').exists({ checkFalsy: true }),
],
{ message: 'At least one field (name, description) must be provided' }
),
body('name')
.optional()
.trim()
.escape()
.isString()
.withMessage('Name must be a string')
.bail()
.isLength({ min: 3 })
.withMessage('Name must be at least 3 characters'),
body('description')
.optional()
.trim()
.escape()
.isString()
.withMessage('Description must be a string'),
handleValidationErrors,
];

View File

@@ -0,0 +1,107 @@
import { param, query, body } from 'express-validator';
import { handleValidationErrors } from './handleValidationErrors.js';
export const validateGetAllQuery = [
query('search')
.optional()
.trim()
.isString()
.withMessage('Search must be a string'),
query('sortBy')
.optional()
.isIn(['name', 'id'])
.withMessage('sortBy must be one of: name, id'),
query('order')
.optional()
.isIn(['asc', 'desc'])
.withMessage('order must be asc or desc'),
query('offset')
.optional()
.isInt({ min: 0 })
.withMessage('offset must be a non-negative integer'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('limit must be an integer between 1 and 100'),
handleValidationErrors,
];
export const validateId = [
param('id')
.trim()
.escape()
.isInt({ min: 1 })
.withMessage('Id must be a positive integer'),
handleValidationErrors,
];
export const validateProjectId = [
param('projectId')
.trim()
.escape()
.isInt({ min: 1 })
.withMessage('ProjectId must be a positive integer'),
handleValidationErrors,
];
export const validateCreateMaterial = [
body('projectId')
.exists({ checkFalsy: true })
.withMessage('ProjectId is required')
.bail()
.isInt({ min: 1 })
.withMessage('ProjectId must be a positive integer'),
body('name')
.exists({ checkFalsy: true })
.withMessage('Name is required')
.bail()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage('Name must be at least 3 characters'),
body('description')
.exists({ checkFalsy: true })
.withMessage('Description is required')
.bail()
.trim()
.escape()
.isString()
.withMessage('Description must be a string'),
body('source')
.exists({ checkFalsy: true })
.withMessage('Source is required')
.bail()
.trim()
.escape()
.isString()
.withMessage('Source must be a string'),
body('author')
.exists({ checkFalsy: true })
.withMessage('Author is required')
.bail()
.trim()
.escape()
.isString()
.withMessage('Author must be a string'),
body('text')
.exists({ checkFalsy: true })
.withMessage('Text is required')
.bail()
.trim()
.escape()
.isString()
.withMessage('Text must be a string'),
handleValidationErrors,
];

View File

@@ -0,0 +1,93 @@
import { param, query, body, oneOf } from 'express-validator';
import { handleValidationErrors } from './handleValidationErrors.js';
export const validateGetAllQuery = [
query('search')
.optional()
.trim()
.isString()
.withMessage('Search must be a string'),
query('sortBy')
.optional()
.isIn(['name', 'date_created'])
.withMessage('sortBy must be one of: name, date_created'),
query('order')
.optional()
.isIn(['asc', 'desc'])
.withMessage('order must be asc or desc'),
query('offset')
.optional()
.isInt({ min: 0 })
.withMessage('offset must be a non-negative integer'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('limit must be an integer between 1 and 100'),
handleValidationErrors,
];
export const validateId = [
param('id')
.trim()
.escape()
.isInt({ min: 1 })
.withMessage('Id must be a positive integer'),
handleValidationErrors,
];
export const validateCreateProject = [
body('name')
.exists({ checkFalsy: true })
.withMessage('Name is required')
.bail()
.trim()
.escape()
.isLength({ min: 3 })
.withMessage('Name must be at least 3 characters'),
body('description')
.exists({ checkFalsy: true })
.withMessage('Description is required')
.bail()
.trim()
.escape()
.isString()
.withMessage('Description must be a string'),
handleValidationErrors,
];
export const validateUpdateProject = [
oneOf(
[
body('name').exists({ checkFalsy: true }),
body('description').exists({ checkFalsy: true }),
],
{ message: 'At least one field (name, description) must be provided' }
),
body('name')
.optional()
.trim()
.escape()
.isString()
.withMessage('Name must be a string')
.bail()
.isLength({ min: 3 })
.withMessage('Name must be at least 3 characters'),
body('description')
.optional()
.trim()
.escape()
.isString()
.withMessage('Description must be a string'),
handleValidationErrors,
];

View File

@@ -0,0 +1,139 @@
import prisma from '../config/db.js';
const ALLOWED_SORT_FIELDS = ['name', 'date_created'];
export async function getAll(
userId,
{
search = '',
sortBy = 'date_created',
order = 'desc',
offset = 0,
limit = 10,
} = {}
) {
if (process.env.NODE_ENV === 'development') {
console.log('ideasRepo.getAll() called for userId:', userId);
}
const safeSortBy = ALLOWED_SORT_FIELDS.includes(sortBy)
? sortBy
: 'date_created';
const safeOrder = order === 'asc' ? 'asc' : 'desc';
const ideas = await prisma.idea.findMany({
where: {
userId,
...(search ? { name: { contains: search, mode: 'insensitive' } } : {}),
},
orderBy: { [safeSortBy]: safeOrder },
skip: offset,
take: limit,
});
return ideas;
}
export async function getById(ideaId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log(
'ideasRepo.getById() called for ideaId:',
ideaId,
'userId:',
userId
);
}
const idea = await prisma.idea.findUnique({
where: { id: ideaId },
});
if (idea && idea.userId !== userId) {
return null;
}
return idea;
}
export async function create(userId, ideaData) {
if (process.env.NODE_ENV === 'development') {
console.log('ideasRepo.create() called with data:', ideaData);
}
const newIdea = await prisma.idea.create({
data: {
...ideaData,
userId,
},
});
return newIdea;
}
export async function update(ideaId, userId, updatedData) {
if (process.env.NODE_ENV === 'development') {
console.log(
'ideasRepo.update() called for ideaId:',
ideaId,
'with data:',
updatedData
);
}
try {
const idea = await prisma.idea.findUnique({
where: { id: ideaId },
});
if (!idea || idea.userId !== userId) {
return null;
}
const updatedIdea = await prisma.idea.update({
where: { id: ideaId },
data: updatedData,
});
return updatedIdea;
} catch (error) {
if (error.code === 'P2025') return null;
throw error;
}
}
export async function remove(ideaId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log('ideasRepo.remove() called for ideaId:', ideaId);
}
try {
const idea = await prisma.idea.findUnique({
where: { id: ideaId },
});
if (!idea || idea.userId !== userId) {
return null;
}
const deletedIdea = await prisma.idea.delete({
where: { id: ideaId },
});
return deletedIdea;
} catch (error) {
if (error.code === 'P2025') return null;
throw error;
}
}
export async function existsByName(userId, name) {
if (process.env.NODE_ENV === 'development') {
console.log('ideasRepo.existsByName() called for name:', name);
}
const idea = await prisma.idea.findFirst({
where: { userId, name },
});
return idea !== null;
}

View File

@@ -0,0 +1,142 @@
import prisma from '../config/db.js';
const ALLOWED_SORT_FIELDS = ['name', 'id'];
export async function getAll(
userId,
{ search = '', sortBy = 'id', order = 'desc', offset = 0, limit = 10 } = {}
) {
if (process.env.NODE_ENV === 'development') {
console.log('materialsRepo.getAll() called for userId:', userId);
}
const safeSortBy = ALLOWED_SORT_FIELDS.includes(sortBy) ? sortBy : 'id';
const safeOrder = order === 'asc' ? 'asc' : 'desc';
const materials = await prisma.material.findMany({
where: {
project: { userId },
...(search ? { name: { contains: search, mode: 'insensitive' } } : {}),
},
orderBy: { [safeSortBy]: safeOrder },
skip: offset,
take: limit,
});
return materials;
}
export async function getByProjectId(
projectId,
userId,
{ search = '', sortBy = 'id', order = 'desc', offset = 0, limit = 10 } = {}
) {
if (process.env.NODE_ENV === 'development') {
console.log(
'materialsRepo.getByProjectId() called for projectId:',
projectId,
'userId:',
userId
);
}
const safeSortBy = ALLOWED_SORT_FIELDS.includes(sortBy) ? sortBy : 'id';
const safeOrder = order === 'asc' ? 'asc' : 'desc';
const materials = await prisma.material.findMany({
where: {
projectId,
project: { userId },
...(search ? { name: { contains: search, mode: 'insensitive' } } : {}),
},
orderBy: { [safeSortBy]: safeOrder },
skip: offset,
take: limit,
});
return materials;
}
export async function getById(materialId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log(
'materialsRepo.getById() called for materialId:',
materialId,
'userId:',
userId
);
}
const material = await prisma.material.findUnique({
where: { id: materialId },
include: { project: true },
});
if (material && material.project.userId !== userId) {
return null;
}
return material;
}
export async function create(userId, materialData) {
if (process.env.NODE_ENV === 'development') {
console.log('materialsRepo.create() called with data:', materialData);
}
try {
const project = await prisma.project.findUnique({
where: { id: materialData.projectId },
});
if (!project || project.userId !== userId) {
return null;
}
const newMaterial = await prisma.material.create({
data: materialData,
});
return newMaterial;
} catch (error) {
throw error;
}
}
export async function remove(materialId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log('materialsRepo.remove() called for materialId:', materialId);
}
try {
const material = await prisma.material.findUnique({
where: { id: materialId },
include: { project: true },
});
if (!material || material.project.userId !== userId) {
return null;
}
const deletedMaterial = await prisma.material.delete({
where: { id: materialId },
});
return deletedMaterial;
} catch (error) {
if (error.code === 'P2025') return null;
throw error;
}
}
export async function existsByName(userId, name) {
if (process.env.NODE_ENV === 'development') {
console.log('materialsRepo.existsByName() called for name:', name);
}
const material = await prisma.material.findFirst({
where: { name, project: { userId } },
});
return material !== null;
}

View File

@@ -0,0 +1,242 @@
import prisma from '../config/db.js';
const ALLOWED_SORT_FIELDS = ['name', 'date_created'];
export async function getAll(
userId,
{
search = '',
sortBy = 'date_created',
order = 'desc',
offset = 0,
limit = 10,
} = {}
) {
if (process.env.NODE_ENV === 'development') {
console.log('projectsRepo.getAll() called for userId:', userId);
}
const safeSortBy = ALLOWED_SORT_FIELDS.includes(sortBy)
? sortBy
: 'date_created';
const safeOrder = order === 'asc' ? 'asc' : 'desc';
const projects = await prisma.project.findMany({
where: {
userId,
...(search ? { name: { contains: search, mode: 'insensitive' } } : {}),
},
orderBy: { [safeSortBy]: safeOrder },
skip: offset,
take: limit,
include: { files: true },
});
return projects;
}
export async function getById(projectId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsRepo.getById() called for projectId:',
projectId,
'userId:',
userId
);
}
const project = await prisma.project.findUnique({
where: { id: projectId },
include: { files: true },
});
if (project && project.userId !== userId) {
return null;
}
return project;
}
export async function create(userId, projectData) {
if (process.env.NODE_ENV === 'development') {
console.log('projectsRepo.create() called with data:', projectData);
}
const newProject = await prisma.project.create({
data: { ...projectData, userId },
include: { files: true },
});
return newProject;
}
export async function update(projectId, userId, updatedData) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsRepo.update() called for projectId:',
projectId,
'with data:',
updatedData
);
}
try {
const project = await prisma.project.findUnique({
where: { id: projectId },
});
if (!project || project.userId !== userId) {
return null;
}
const updatedProject = await prisma.project.update({
where: { id: projectId },
data: updatedData,
include: { files: true },
});
return updatedProject;
} catch (error) {
if (error.code === 'P2025') return null;
throw error;
}
}
export async function remove(projectId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log('projectsRepo.remove() called for projectId:', projectId);
}
try {
const project = await prisma.project.findUnique({
where: { id: projectId },
});
if (!project || project.userId !== userId) {
return null;
}
const deletedProject = await prisma.project.delete({
where: { id: projectId },
include: { files: true },
});
return deletedProject;
} catch (error) {
if (error.code === 'P2025') return null;
throw error;
}
}
export async function existsByName(userId, name) {
if (process.env.NODE_ENV === 'development') {
console.log('projectsRepo.existsByName() called for name:', name);
}
const project = await prisma.project.findFirst({
where: { userId, name },
});
return project !== null;
}
export async function getFilesByProjectId(projectId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsRepo.getFilesByProjectId() called for projectId:',
projectId,
'userId:',
userId
);
}
try {
const project = await prisma.project.findUnique({
where: { id: projectId },
});
if (!project || project.userId !== userId) {
return null;
}
const files = await prisma.file.findMany({
where: { projectId },
});
return files;
} catch (error) {
throw error;
}
}
export async function addFile(projectId, userId, fileData) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsRepo.addFile() called for projectId:',
projectId,
'userId:',
userId,
'with data:',
fileData
);
}
const project = await prisma.project.findUnique({
where: { id: projectId },
});
if (!project || project.userId !== userId) {
return null;
}
const newFile = await prisma.file.create({
data: {
name: fileData.name,
file: fileData.file,
size: fileData.size,
mimeType: fileData.mimeType,
projectId: projectId
}
});
return newFile;
}
export async function deleteFile(fileId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log('projectsRepo.deleteFile() called for fileId:', fileId, 'userId:', userId);
}
const file = await prisma.file.findUnique({
where: { id: fileId },
include: { project: true }
});
if (!file || file.project.userId !== userId) {
return null;
}
return await prisma.file.delete({
where: { id: fileId },
});
}
export async function getFilesWithContent(projectId, userId) {
const project = await prisma.project.findUnique({
where: { id: projectId },
});
if (!project || project.userId !== userId) return null;
return await prisma.file.findMany({
where: { projectId },
select: {
id: true,
name: true,
file: true,
size: true,
mimeType: true,
projectId: true
}
});
}

View File

@@ -0,0 +1,30 @@
import prisma from '../config/db.js';
export async function createUser(data) {
if (process.env.NODE_ENV === 'development') {
console.log('userRepo.createUser() called for username:', data.username);
}
try {
const newUser = await prisma.user.create({
data,
omit: { password: true },
});
return newUser;
} catch (error) {
if (error.code === 'P2002') {
const err = new Error('Username already exists');
err.status = 409;
throw err;
}
throw error;
}
}
export async function findUserByUsername(username) {
if (process.env.NODE_ENV === 'development') {
console.log('userRepo.findUserByUsername() called for username:', username);
}
return await prisma.user.findUnique({ where: { username } });
}

10
src/routes/authRoutes.js Normal file
View File

@@ -0,0 +1,10 @@
import express from 'express';
import { logInHandler, signUpHandler } from '../controllers/authController.js';
import { validateSignUp, validateLogIn } from '../middleware/validateAuth.js';
const router = express.Router();
router.post('/signup', validateSignUp, signUpHandler);
router.post('/login', validateLogIn, logInHandler);
export default router;

25
src/routes/ideasRoutes.js Normal file
View File

@@ -0,0 +1,25 @@
import express from 'express';
import {
getAllIdeasHandler,
createIdeaHandler,
updateIdeaHandler,
deleteIdeaHandler,
} from '../controllers/ideasController.js';
import {
validateGetAllQuery,
validateId,
validateCreateIdea,
validateUpdateIdea,
} from '../middleware/validateIdeas.js';
import { authenticate } from '../middleware/authenticate.js';
const router = express.Router();
router.use(authenticate);
router.get('/', validateGetAllQuery, getAllIdeasHandler);
router.post('/', validateCreateIdea, createIdeaHandler);
router.put('/:id', validateId, validateUpdateIdea, updateIdeaHandler);
router.delete('/:id', validateId, deleteIdeaHandler);
export default router;

View File

@@ -0,0 +1,32 @@
import express from 'express';
import {
getAllMaterialsHandler,
getMaterialsByProjectHandler,
getMaterialByIdHandler,
createMaterialHandler,
deleteMaterialHandler,
} from '../controllers/materialsController.js';
import {
validateGetAllQuery,
validateId,
validateProjectId,
validateCreateMaterial,
} from '../middleware/validateMaterials.js';
import { authenticate } from '../middleware/authenticate.js';
const router = express.Router();
router.use(authenticate);
router.get('/', validateGetAllQuery, getAllMaterialsHandler);
router.get(
'/project/:projectId',
validateProjectId,
validateGetAllQuery,
getMaterialsByProjectHandler
);
router.get('/:id', validateId, getMaterialByIdHandler);
router.post('/', validateCreateMaterial, createMaterialHandler);
router.delete('/:id', validateId, deleteMaterialHandler);
export default router;

View File

@@ -0,0 +1,44 @@
import express from 'express';
import multer from 'multer';
import {
getAllProjectsHandler,
getProjectByIdHandler,
createProjectHandler,
updateProjectHandler,
deleteProjectHandler,
getProjectFilesHandler,
addProjectFileHandler,
deleteProjectFileHandler,
downloadProjectFilesHandler
} from '../controllers/projectsController.js';
import {
validateGetAllQuery,
validateId,
validateCreateProject,
validateUpdateProject,
} from '../middleware/validateProjects.js';
import { authenticate } from '../middleware/authenticate.js';
const router = express.Router();
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 }
});
router.use(authenticate);
//Project Routes
router.get('/', validateGetAllQuery, getAllProjectsHandler);
router.get('/:id', validateId, getProjectByIdHandler);
router.post('/', validateCreateProject, createProjectHandler);
router.put('/:id', validateId, validateUpdateProject, updateProjectHandler);
router.delete('/:id', validateId, deleteProjectHandler);
// Project File Routes
router.get('/:id/files', validateId, getProjectFilesHandler);
router.post('/:id/files', validateId, upload.single('file'), addProjectFileHandler);
router.delete('/files/:id', validateId, deleteProjectFileHandler);
router.get('/:id/files/download', validateId, downloadProjectFilesHandler);
export default router;

57
src/server.js Normal file
View File

@@ -0,0 +1,57 @@
import express from 'express';
import morgan from 'morgan';
import fs from 'fs';
import swaggerUI from 'swagger-ui-express';
import yaml from 'js-yaml';
import authRoutes from './routes/authRoutes.js';
import ideasRoutes from './routes/ideasRoutes.js';
import projectsRoutes from './routes/projectsRoutes.js';
import materialsRoutes from './routes/materialsRoutes.js';
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
if (process.env.NODE_ENV !== 'test') app.use(morgan('tiny'));
let specs;
try {
specs = yaml.load(fs.readFileSync('./docs/openapi.yaml', 'utf8'));
} catch (error) {
console.log('Failed to load OpenAPI specification', error);
process.exit(1);
}
app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(specs));
app.use('/api/auth', authRoutes);
app.use('/api/ideas', ideasRoutes);
app.use('/api/projects', projectsRoutes);
app.use('/api/materials', materialsRoutes);
app.use((req, res, next) => {
const err = new Error('Not Found');
err.status = 404;
next(err);
});
app.use((err, req, res, next) => {
if (err.code === "LIMIT_FILE_SIZE") {
return res.status(413).json({
error: 'File size cannot exceed 10MB'
});
}
console.log(err.stack);
if (!err.status) {
err.status = 500;
err.message = 'Internal Server Error';
}
res.status(err.status).json({ error: err.message });
});
if (process.env.NODE_ENV !== 'test') {
app.listen(PORT, () => console.log(`Server is running on port ${PORT}`));
}
export default app;

View File

@@ -0,0 +1,36 @@
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { createUser, findUserByUsername } from '../repositories/userRepo.js';
export async function signUp(username, password) {
if (process.env.NODE_ENV === 'development') {
console.log('authService.signUp() called for username:', username);
}
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = await createUser({ username, password: hashedPassword });
return newUser;
}
export async function logIn(username, password) {
if (process.env.NODE_ENV === 'development') {
console.log('authService.logIn() called for username:', username);
}
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN;
const user = await findUserByUsername(username);
const error = new Error('Invalid credentials');
error.status = 401;
if (!user) throw error;
const match = await bcrypt.compare(password, user.password);
if (!match) throw error;
const accessToken = jwt.sign({ id: user.id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
return accessToken;
}

View File

@@ -0,0 +1,88 @@
import {
getAll,
getById,
create,
update,
remove,
existsByName,
} from '../repositories/ideasRepo.js';
export async function getAllIdeas(userId, options = {}) {
if (process.env.NODE_ENV === 'development') {
console.log('ideasService.getAllIdeas() called for userId:', userId);
}
const ideas = await getAll(userId, options);
return ideas;
}
export async function getIdeaById(ideaId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log('ideasService.getIdeaById() called for ideaId:', ideaId);
}
const idea = await getById(ideaId, userId);
if (idea) return idea;
const error = new Error(`Idea ${ideaId} not found`);
error.status = 404;
throw error;
}
export async function createIdea(userId, ideaData) {
if (process.env.NODE_ENV === 'development') {
console.log('ideasService.createIdea() called with data:', ideaData);
}
const ideaExists = await existsByName(userId, ideaData.name);
if (ideaExists) {
const error = new Error(`Idea with name "${ideaData.name}" already exists`);
error.status = 409;
throw error;
}
const newIdea = await create(userId, ideaData);
return newIdea;
}
export async function updateIdea(ideaId, userId, updatedData) {
if (process.env.NODE_ENV === 'development') {
console.log(
'ideasService.updateIdea() called for ideaId:',
ideaId,
'with data:',
updatedData
);
}
if (updatedData.name) {
const ideaExists = await existsByName(userId, updatedData.name);
if (ideaExists) {
const error = new Error(
`Idea with name "${updatedData.name}" already exists`
);
error.status = 409;
throw error;
}
}
const updatedIdea = await update(ideaId, userId, updatedData);
if (updatedIdea) return updatedIdea;
const error = new Error(`Idea ${ideaId} not found`);
error.status = 404;
throw error;
}
export async function deleteIdea(ideaId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log('ideasService.deleteIdea() called for ideaId:', ideaId);
}
const result = await remove(ideaId, userId);
if (result) return;
const error = new Error(`Idea ${ideaId} not found`);
error.status = 404;
throw error;
}

View File

@@ -0,0 +1,85 @@
import {
getAll,
getByProjectId,
getById,
create,
remove,
existsByName,
} from '../repositories/materialsRepo.js';
export async function getAllMaterials(userId, options = {}) {
if (process.env.NODE_ENV === 'development') {
console.log(
'materialsService.getAllMaterials() called for userId:',
userId
);
}
const materials = await getAll(userId, options);
return materials;
}
export async function getMaterialsByProject(projectId, userId, options = {}) {
if (process.env.NODE_ENV === 'development') {
console.log(
'materialsService.getMaterialsByProject() called for projectId:',
projectId
);
}
const materials = await getByProjectId(projectId, userId, options);
return materials;
}
export async function getMaterialById(materialId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log(
'materialsService.getMaterialById() called for materialId:',
materialId
);
}
const material = await getById(materialId, userId);
if (material) return material;
const error = new Error(`Material ${materialId} not found`);
error.status = 404;
throw error;
}
export async function createMaterial(userId, materialData) {
if (process.env.NODE_ENV === 'development') {
console.log(
'materialsService.createMaterial() called with data:',
materialData
);
}
const materialExists = await existsByName(userId, materialData.name);
if (materialExists) {
const error = new Error(
`Material with name "${materialData.name}" already exists`
);
error.status = 409;
throw error;
}
const newMaterial = await create(userId, materialData);
return newMaterial;
}
export async function deleteMaterial(materialId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log(
'materialsService.deleteMaterial() called for materialId:',
materialId
);
}
const result = await remove(materialId, userId);
if (result) return;
const error = new Error(`Material ${materialId} not found`);
error.status = 404;
throw error;
}

View File

@@ -0,0 +1,155 @@
import {
getAll,
getById,
create,
update,
remove,
existsByName,
getFilesByProjectId,
getFilesWithContent,
addFile,
deleteFile
} from '../repositories/projectsRepo.js';
export async function getAllProjects(userId, options = {}) {
if (process.env.NODE_ENV === 'development') {
console.log('projectsService.getAllProjects() called for userId:', userId);
}
return await getAll(userId, options);
}
export async function getProjectById(projectId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsService.getProjectById() called for projectId:',
projectId
);
}
const project = await getById(projectId, userId);
if (project) return project;
const error = new Error(`Project ${projectId} not found`);
error.status = 404;
throw error;
}
export async function createProject(userId, projectData) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsService.createProject() called with data:',
projectData
);
}
const projectExists = await existsByName(userId, projectData.name);
if (projectExists) {
const error = new Error(
`Project with name "${projectData.name}" already exists`
);
error.status = 409;
throw error;
}
const newProject = await create(userId, projectData);
return newProject;
}
export async function updateProject(projectId, userId, updatedData) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsService.updateProject() called for projectId:',
projectId,
'with data:',
updatedData
);
}
if (updatedData.name) {
const projectExists = await existsByName(userId, updatedData.name);
if (projectExists) {
const error = new Error(
`Project with name "${updatedData.name}" already exists`
);
error.status = 409;
throw error;
}
}
const updatedProject = await update(projectId, userId, updatedData);
if (updatedProject) return updatedProject;
const error = new Error(`Project ${projectId} not found`);
error.status = 404;
throw error;
}
export async function deleteProject(projectId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsService.deleteProject() called for projectId:',
projectId
);
}
const result = await remove(projectId, userId);
if (result) return;
const error = new Error(`Project ${projectId} not found`);
error.status = 404;
throw error;
}
export async function getProjectFilesById(projectId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsService.getProjectFilesById() called for projectId:',
projectId
);
}
const files = await getFilesByProjectId(projectId, userId);
if (files) return files;
const error = new Error(`Project ${projectId} not found`);
error.status = 404;
throw error;
}
export async function getProjectFilesForDownload(projectId, userId) {
const files = await getFilesWithContent(projectId, userId);
if (!files) {
const error = new Error(`Project ${projectId} not found or unauthorized`);
error.status = 404;
throw error;
}
return files;
}
export async function addProjectFile(projectId, userId, fileData) {
if (process.env.NODE_ENV === 'development') {
console.log(
'projectsService.addProjectFile() called for projectId:',
projectId,
'with fileData:',
fileData
);
}
const newFile = await addFile(projectId, userId, fileData);
if (!newFile) {
const error = new Error(`Project ${projectId} not found or unauthorized`);
error.status = 404;
throw error;
}
return newFile;
}
export async function deleteProjectFile(fileId, userId) {
if (process.env.NODE_ENV === 'development') {
console.log('projectsService.deleteProjectFile() called for fileId:', fileId);
}
const deletedFile = await deleteFile(fileId, userId);
if (deletedFile) return deletedFile;
}