@studious-lms/server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +8 -0
- package/README.md +143 -0
- package/dist/exportType.d.ts +9 -0
- package/dist/exportType.d.ts.map +1 -0
- package/dist/exportType.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/lib/fileUpload.d.ts +38 -0
- package/dist/lib/fileUpload.d.ts.map +1 -0
- package/dist/lib/fileUpload.js +136 -0
- package/dist/lib/googleCloudStorage.d.ts +20 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -0
- package/dist/lib/googleCloudStorage.js +88 -0
- package/dist/lib/prisma.d.ts +8 -0
- package/dist/lib/prisma.d.ts.map +1 -0
- package/dist/lib/prisma.js +11 -0
- package/dist/lib/thumbnailGenerator.d.ts +23 -0
- package/dist/lib/thumbnailGenerator.d.ts.map +1 -0
- package/dist/lib/thumbnailGenerator.js +180 -0
- package/dist/logger.d.ts +26 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +135 -0
- package/dist/middleware/auth.d.ts +7 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +170 -0
- package/dist/middleware/logging.d.ts +2 -0
- package/dist/middleware/logging.d.ts.map +1 -0
- package/dist/middleware/logging.js +51 -0
- package/dist/routers/_app.d.ts +4369 -0
- package/dist/routers/_app.d.ts.map +1 -0
- package/dist/routers/_app.js +29 -0
- package/dist/routers/agenda.d.ts +66 -0
- package/dist/routers/agenda.d.ts.map +1 -0
- package/dist/routers/agenda.js +78 -0
- package/dist/routers/announcement.d.ts +79 -0
- package/dist/routers/announcement.d.ts.map +1 -0
- package/dist/routers/announcement.js +122 -0
- package/dist/routers/assignment.d.ts +1051 -0
- package/dist/routers/assignment.d.ts.map +1 -0
- package/dist/routers/assignment.js +1497 -0
- package/dist/routers/attendance.d.ts +99 -0
- package/dist/routers/attendance.d.ts.map +1 -0
- package/dist/routers/attendance.js +269 -0
- package/dist/routers/auth.d.ts +87 -0
- package/dist/routers/auth.d.ts.map +1 -0
- package/dist/routers/auth.js +255 -0
- package/dist/routers/class.d.ts +427 -0
- package/dist/routers/class.d.ts.map +1 -0
- package/dist/routers/class.js +693 -0
- package/dist/routers/event.d.ts +249 -0
- package/dist/routers/event.d.ts.map +1 -0
- package/dist/routers/event.js +467 -0
- package/dist/routers/file.d.ts +27 -0
- package/dist/routers/file.d.ts.map +1 -0
- package/dist/routers/file.js +88 -0
- package/dist/routers/section.d.ts +51 -0
- package/dist/routers/section.d.ts.map +1 -0
- package/dist/routers/section.js +123 -0
- package/dist/routers/user.d.ts +49 -0
- package/dist/routers/user.d.ts.map +1 -0
- package/dist/routers/user.js +74 -0
- package/dist/socket/handlers.d.ts +3 -0
- package/dist/socket/handlers.d.ts.map +1 -0
- package/dist/socket/handlers.js +130 -0
- package/dist/trpc.d.ts +147 -0
- package/dist/trpc.d.ts.map +1 -0
- package/dist/trpc.js +67 -0
- package/dist/types/trpc.d.ts +16 -0
- package/dist/types/trpc.d.ts.map +1 -0
- package/dist/types/trpc.js +2 -0
- package/dist/utils/email.d.ts +3 -0
- package/dist/utils/email.d.ts.map +1 -0
- package/dist/utils/email.js +16 -0
- package/dist/utils/generateInviteCode.d.ts +6 -0
- package/dist/utils/generateInviteCode.d.ts.map +1 -0
- package/dist/utils/generateInviteCode.js +11 -0
- package/dist/utils/logger.d.ts +27 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +124 -0
- package/generated/prisma/client.d.ts +1 -0
- package/generated/prisma/client.js +4 -0
- package/generated/prisma/default.d.ts +1 -0
- package/generated/prisma/default.js +4 -0
- package/generated/prisma/edge.d.ts +1 -0
- package/generated/prisma/edge.js +389 -0
- package/generated/prisma/index-browser.js +375 -0
- package/generated/prisma/index.d.ts +34865 -0
- package/generated/prisma/index.js +410 -0
- package/generated/prisma/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/generated/prisma/package.json +140 -0
- package/generated/prisma/runtime/edge-esm.js +34 -0
- package/generated/prisma/runtime/edge.js +34 -0
- package/generated/prisma/runtime/index-browser.d.ts +370 -0
- package/generated/prisma/runtime/index-browser.js +16 -0
- package/generated/prisma/runtime/library.d.ts +3647 -0
- package/generated/prisma/runtime/library.js +146 -0
- package/generated/prisma/runtime/react-native.js +83 -0
- package/generated/prisma/runtime/wasm.js +35 -0
- package/generated/prisma/schema.prisma +304 -0
- package/generated/prisma/wasm.d.ts +1 -0
- package/generated/prisma/wasm.js +375 -0
- package/package.json +46 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright 2025 Alan Shen <contact@alanshen.me>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
8
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Studious Backend Server
|
|
2
|
+
|
|
3
|
+
This is the backend server for the Studious application. It provides a RESTful and real-time API for managing users, classes, assignments, attendance, and more, supporting both HTTP (via tRPC) and WebSocket (via Socket.IO) communication.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Express.js** server with CORS support
|
|
8
|
+
- **tRPC** for type-safe API endpoints
|
|
9
|
+
- **Socket.IO** for real-time features
|
|
10
|
+
- **Prisma ORM** with PostgreSQL for database management
|
|
11
|
+
- **User authentication** and session management
|
|
12
|
+
- **Class, assignment, attendance, and file management**
|
|
13
|
+
- **Structured logging** for requests and server events
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
### Prerequisites
|
|
18
|
+
|
|
19
|
+
- Node.js (v18+ recommended)
|
|
20
|
+
- npm (v9+ recommended)
|
|
21
|
+
- PostgreSQL database
|
|
22
|
+
|
|
23
|
+
### Installation
|
|
24
|
+
|
|
25
|
+
1. **Clone the repository:**
|
|
26
|
+
```bash
|
|
27
|
+
git clone <repo-url>
|
|
28
|
+
cd server
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. **Install dependencies:**
|
|
32
|
+
```bash
|
|
33
|
+
npm install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. **Set up environment variables:**
|
|
37
|
+
Create a `.env` file in the root directory with the following variables:
|
|
38
|
+
```env
|
|
39
|
+
DATABASE_URL="postgresql://username:password@localhost:5432/easy_lms"
|
|
40
|
+
PORT=3001
|
|
41
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
42
|
+
NODE_ENV=development
|
|
43
|
+
LOG_MODE=info
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
4. **Set up the database:**
|
|
47
|
+
- Create a PostgreSQL database named `easy_lms`
|
|
48
|
+
- Run the Prisma migrations:
|
|
49
|
+
```bash
|
|
50
|
+
npx prisma migrate dev --name init
|
|
51
|
+
```
|
|
52
|
+
- Generate the Prisma client:
|
|
53
|
+
```bash
|
|
54
|
+
npx prisma generate
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Running the Server
|
|
58
|
+
|
|
59
|
+
- **Development mode (with hot reload):**
|
|
60
|
+
```bash
|
|
61
|
+
npm run dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- **Production build:**
|
|
65
|
+
```bash
|
|
66
|
+
npm run build
|
|
67
|
+
npm start
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The server will start on the port specified in your `.env` file (default: `3001`).
|
|
71
|
+
|
|
72
|
+
## API Overview
|
|
73
|
+
|
|
74
|
+
- **tRPC endpoints:** Available at `/trpc`
|
|
75
|
+
- **WebSocket:** Available at `/socket.io/`
|
|
76
|
+
- **CORS:** Configured to allow requests from the frontend app URL via `NEXT_PUBLIC_APP_URL`
|
|
77
|
+
|
|
78
|
+
## Project Structure
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
src/
|
|
82
|
+
index.ts # Main server entry point
|
|
83
|
+
routers/ # tRPC routers for API endpoints
|
|
84
|
+
socket/ # Socket.IO event handlers
|
|
85
|
+
middleware/ # Express and tRPC middleware
|
|
86
|
+
utils/ # Utility functions (e.g., logger)
|
|
87
|
+
lib/ # Shared libraries
|
|
88
|
+
types/ # TypeScript types
|
|
89
|
+
prisma/
|
|
90
|
+
schema.prisma # Prisma database schema
|
|
91
|
+
migrations/ # Database migrations
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Database
|
|
95
|
+
|
|
96
|
+
- **Database:** PostgreSQL
|
|
97
|
+
- **ORM:** Prisma
|
|
98
|
+
- **Schema:** Defined in `prisma/schema.prisma`
|
|
99
|
+
- **Migrations:** Stored in `prisma/migrations/`
|
|
100
|
+
|
|
101
|
+
### Database Commands
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Generate Prisma client
|
|
105
|
+
npx prisma generate
|
|
106
|
+
|
|
107
|
+
# Run migrations
|
|
108
|
+
npx prisma migrate dev
|
|
109
|
+
|
|
110
|
+
# Reset database
|
|
111
|
+
npx prisma migrate reset
|
|
112
|
+
|
|
113
|
+
# Open Prisma Studio (database GUI)
|
|
114
|
+
npx prisma studio
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Scripts
|
|
118
|
+
|
|
119
|
+
- `npm run dev` — Start in development mode with hot reload
|
|
120
|
+
- `npm run build` — Compile TypeScript to JavaScript
|
|
121
|
+
- `npm start` — Start the compiled server
|
|
122
|
+
|
|
123
|
+
## Environment Variables
|
|
124
|
+
|
|
125
|
+
| Variable | Description | Default |
|
|
126
|
+
|----------|-------------|---------|
|
|
127
|
+
| `DATABASE_URL` | PostgreSQL connection string | Required |
|
|
128
|
+
| `PORT` | Server port | 3001 |
|
|
129
|
+
| `NEXT_PUBLIC_APP_URL` | Frontend app URL for CORS | http://localhost:3000 |
|
|
130
|
+
| `NODE_ENV` | Environment mode | development |
|
|
131
|
+
| `LOG_MODE` | Logging level | info |
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
The server uses TypeScript and includes:
|
|
136
|
+
- **tRPC** for type-safe API development
|
|
137
|
+
- **Socket.IO** for real-time communication
|
|
138
|
+
- **Prisma** for database operations
|
|
139
|
+
- **Express** middleware for CORS and logging
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
[MIT](LICENSE.txt)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export types for the server
|
|
3
|
+
* This is used to export the types for the server
|
|
4
|
+
* to the client via npmjs
|
|
5
|
+
*/
|
|
6
|
+
export type { AppRouter } from "@/routers/_app";
|
|
7
|
+
export type { RouterInputs } from "@/routers/_app";
|
|
8
|
+
export type { RouterOutputs } from "@/routers/_app";
|
|
9
|
+
//# sourceMappingURL=exportType.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exportType.d.ts","sourceRoot":"","sources":["../src/exportType.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const http_1 = require("http");
|
|
8
|
+
const socket_io_1 = require("socket.io");
|
|
9
|
+
const cors_1 = __importDefault(require("cors"));
|
|
10
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
11
|
+
const express_2 = require("@trpc/server/adapters/express");
|
|
12
|
+
const _app_1 = require("@routers/_app");
|
|
13
|
+
const trpc_1 = require("@/trpc");
|
|
14
|
+
const logger_1 = require("@utils/logger");
|
|
15
|
+
const handlers_1 = require("@/socket/handlers");
|
|
16
|
+
dotenv_1.default.config();
|
|
17
|
+
const app = (0, express_1.default)();
|
|
18
|
+
// CORS middleware
|
|
19
|
+
app.use((0, cors_1.default)({
|
|
20
|
+
origin: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
|
|
21
|
+
credentials: true,
|
|
22
|
+
}));
|
|
23
|
+
// Response time logging middleware
|
|
24
|
+
app.use((req, res, next) => {
|
|
25
|
+
const start = Date.now();
|
|
26
|
+
res.on('finish', () => {
|
|
27
|
+
const duration = Date.now() - start;
|
|
28
|
+
logger_1.logger.info('Request completed', {
|
|
29
|
+
method: req.method,
|
|
30
|
+
path: req.path,
|
|
31
|
+
statusCode: res.statusCode,
|
|
32
|
+
duration: `${duration}ms`
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
// Create HTTP server
|
|
38
|
+
const httpServer = (0, http_1.createServer)(app);
|
|
39
|
+
// Setup Socket.IO
|
|
40
|
+
const io = new socket_io_1.Server(httpServer, {
|
|
41
|
+
cors: {
|
|
42
|
+
origin: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
|
|
43
|
+
methods: ['GET', 'POST'],
|
|
44
|
+
credentials: true,
|
|
45
|
+
allowedHeaders: ['Access-Control-Allow-Origin']
|
|
46
|
+
},
|
|
47
|
+
transports: ['websocket', 'polling'],
|
|
48
|
+
pingTimeout: 60000,
|
|
49
|
+
pingInterval: 25000,
|
|
50
|
+
connectTimeout: 45000,
|
|
51
|
+
path: '/socket.io/',
|
|
52
|
+
allowEIO3: true
|
|
53
|
+
});
|
|
54
|
+
// Add server-level logging
|
|
55
|
+
io.engine.on('connection_error', (err) => {
|
|
56
|
+
logger_1.logger.error('Socket connection error', { error: err.message });
|
|
57
|
+
});
|
|
58
|
+
// Setup socket handlers
|
|
59
|
+
(0, handlers_1.setupSocketHandlers)(io);
|
|
60
|
+
// Create caller
|
|
61
|
+
const createCaller = (0, trpc_1.createCallerFactory)(_app_1.appRouter);
|
|
62
|
+
// Setup tRPC middleware
|
|
63
|
+
app.use('/trpc', (0, express_2.createExpressMiddleware)({
|
|
64
|
+
router: _app_1.appRouter,
|
|
65
|
+
createContext: async ({ req, res }) => {
|
|
66
|
+
return (0, trpc_1.createTRPCContext)({ req, res });
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
const PORT = process.env.PORT || 3001;
|
|
70
|
+
httpServer.listen(PORT, () => {
|
|
71
|
+
logger_1.logger.info(`Server running on port ${PORT}`, {
|
|
72
|
+
port: PORT,
|
|
73
|
+
services: ['tRPC', 'Socket.IO']
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// log all env variables
|
|
77
|
+
logger_1.logger.info('Configurations', {
|
|
78
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
79
|
+
PORT: process.env.PORT,
|
|
80
|
+
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
|
81
|
+
LOG_MODE: process.env.LOG_MODE,
|
|
82
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface FileData {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
size: number;
|
|
5
|
+
data: string;
|
|
6
|
+
}
|
|
7
|
+
export interface UploadedFile {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
type: string;
|
|
11
|
+
size: number;
|
|
12
|
+
path: string;
|
|
13
|
+
thumbnailId?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Uploads a single file to Google Cloud Storage and creates a file record
|
|
17
|
+
* @param file The file data to upload
|
|
18
|
+
* @param userId The ID of the user uploading the file
|
|
19
|
+
* @param directory Optional directory to store the file in
|
|
20
|
+
* @param assignmentId Optional assignment ID to associate the file with
|
|
21
|
+
* @returns The uploaded file record
|
|
22
|
+
*/
|
|
23
|
+
export declare function uploadFile(file: FileData, userId: string, directory?: string, assignmentId?: string): Promise<UploadedFile>;
|
|
24
|
+
/**
|
|
25
|
+
* Uploads multiple files
|
|
26
|
+
* @param files Array of files to upload
|
|
27
|
+
* @param userId The ID of the user uploading the files
|
|
28
|
+
* @param directory Optional subdirectory to store the files in
|
|
29
|
+
* @returns Array of uploaded file information
|
|
30
|
+
*/
|
|
31
|
+
export declare function uploadFiles(files: FileData[], userId: string, directory?: string): Promise<UploadedFile[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Gets a signed URL for a file
|
|
34
|
+
* @param filePath The path of the file in Google Cloud Storage
|
|
35
|
+
* @returns The signed URL
|
|
36
|
+
*/
|
|
37
|
+
export declare function getFileUrl(filePath: string): Promise<string>;
|
|
38
|
+
//# sourceMappingURL=fileUpload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileUpload.d.ts","sourceRoot":"","sources":["../../src/lib/fileUpload.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,YAAY,CAAC,CAsFvB;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAWzB;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUlE"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.uploadFile = uploadFile;
|
|
4
|
+
exports.uploadFiles = uploadFiles;
|
|
5
|
+
exports.getFileUrl = getFileUrl;
|
|
6
|
+
const server_1 = require("@trpc/server");
|
|
7
|
+
const uuid_1 = require("uuid");
|
|
8
|
+
const googleCloudStorage_1 = require("./googleCloudStorage");
|
|
9
|
+
const thumbnailGenerator_1 = require("./thumbnailGenerator");
|
|
10
|
+
const prisma_1 = require("./prisma");
|
|
11
|
+
/**
|
|
12
|
+
* Uploads a single file to Google Cloud Storage and creates a file record
|
|
13
|
+
* @param file The file data to upload
|
|
14
|
+
* @param userId The ID of the user uploading the file
|
|
15
|
+
* @param directory Optional directory to store the file in
|
|
16
|
+
* @param assignmentId Optional assignment ID to associate the file with
|
|
17
|
+
* @returns The uploaded file record
|
|
18
|
+
*/
|
|
19
|
+
async function uploadFile(file, userId, directory, assignmentId) {
|
|
20
|
+
try {
|
|
21
|
+
// Create a unique filename
|
|
22
|
+
const fileExtension = file.name.split('.').pop();
|
|
23
|
+
const uniqueFilename = `${(0, uuid_1.v4)()}.${fileExtension}`;
|
|
24
|
+
// Construct the full path
|
|
25
|
+
const filePath = directory
|
|
26
|
+
? `${directory}/${uniqueFilename}`
|
|
27
|
+
: uniqueFilename;
|
|
28
|
+
// Upload to Google Cloud Storage
|
|
29
|
+
const uploadedPath = await (0, googleCloudStorage_1.uploadFile)(file.data, filePath, file.type);
|
|
30
|
+
// Generate and store thumbnail if supported
|
|
31
|
+
let thumbnailId;
|
|
32
|
+
try {
|
|
33
|
+
// Convert base64 to buffer for thumbnail generation
|
|
34
|
+
const base64Data = file.data.split(',')[1];
|
|
35
|
+
const fileBuffer = Buffer.from(base64Data, 'base64');
|
|
36
|
+
// Generate thumbnail directly from buffer
|
|
37
|
+
const thumbnailBuffer = await (0, thumbnailGenerator_1.generateMediaThumbnail)(fileBuffer, file.type);
|
|
38
|
+
if (thumbnailBuffer) {
|
|
39
|
+
// Store thumbnail in a thumbnails directory
|
|
40
|
+
const thumbnailPath = `thumbnails/${filePath}`;
|
|
41
|
+
const thumbnailBase64 = `data:image/jpeg;base64,${thumbnailBuffer.toString('base64')}`;
|
|
42
|
+
await (0, googleCloudStorage_1.uploadFile)(thumbnailBase64, thumbnailPath, 'image/jpeg');
|
|
43
|
+
// Create thumbnail file record
|
|
44
|
+
const thumbnailFile = await prisma_1.prisma.file.create({
|
|
45
|
+
data: {
|
|
46
|
+
name: `${file.name}_thumb.jpg`,
|
|
47
|
+
type: 'image/jpeg',
|
|
48
|
+
path: thumbnailPath,
|
|
49
|
+
user: {
|
|
50
|
+
connect: { id: userId }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
thumbnailId = thumbnailFile.id;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.warn('Failed to generate thumbnail:', error);
|
|
59
|
+
}
|
|
60
|
+
// Create file record in database
|
|
61
|
+
const fileRecord = await prisma_1.prisma.file.create({
|
|
62
|
+
data: {
|
|
63
|
+
name: file.name,
|
|
64
|
+
type: file.type,
|
|
65
|
+
size: file.size,
|
|
66
|
+
path: uploadedPath,
|
|
67
|
+
user: {
|
|
68
|
+
connect: { id: userId }
|
|
69
|
+
},
|
|
70
|
+
...(thumbnailId && {
|
|
71
|
+
thumbnail: {
|
|
72
|
+
connect: { id: thumbnailId }
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
...(assignmentId && {
|
|
76
|
+
assignment: {
|
|
77
|
+
connect: { id: assignmentId }
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
// Return file information
|
|
83
|
+
return {
|
|
84
|
+
id: fileRecord.id,
|
|
85
|
+
name: file.name,
|
|
86
|
+
type: file.type,
|
|
87
|
+
size: file.size,
|
|
88
|
+
path: uploadedPath,
|
|
89
|
+
thumbnailId: thumbnailId
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error('Error uploading file:', error);
|
|
94
|
+
throw new server_1.TRPCError({
|
|
95
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
96
|
+
message: 'Failed to upload file',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Uploads multiple files
|
|
102
|
+
* @param files Array of files to upload
|
|
103
|
+
* @param userId The ID of the user uploading the files
|
|
104
|
+
* @param directory Optional subdirectory to store the files in
|
|
105
|
+
* @returns Array of uploaded file information
|
|
106
|
+
*/
|
|
107
|
+
async function uploadFiles(files, userId, directory) {
|
|
108
|
+
try {
|
|
109
|
+
const uploadPromises = files.map(file => uploadFile(file, userId, directory));
|
|
110
|
+
return await Promise.all(uploadPromises);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.error('Error uploading files:', error);
|
|
114
|
+
throw new server_1.TRPCError({
|
|
115
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
116
|
+
message: 'Failed to upload files',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Gets a signed URL for a file
|
|
122
|
+
* @param filePath The path of the file in Google Cloud Storage
|
|
123
|
+
* @returns The signed URL
|
|
124
|
+
*/
|
|
125
|
+
async function getFileUrl(filePath) {
|
|
126
|
+
try {
|
|
127
|
+
return await (0, googleCloudStorage_1.getSignedUrl)(filePath);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error('Error getting signed URL:', error);
|
|
131
|
+
throw new server_1.TRPCError({
|
|
132
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
133
|
+
message: 'Failed to get file URL',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uploads a file to Google Cloud Storage
|
|
3
|
+
* @param base64Data Base64 encoded file data
|
|
4
|
+
* @param filePath The path where the file should be stored
|
|
5
|
+
* @param contentType The MIME type of the file
|
|
6
|
+
* @returns The path of the uploaded file
|
|
7
|
+
*/
|
|
8
|
+
export declare function uploadFile(base64Data: string, filePath: string, contentType: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Gets a signed URL for a file
|
|
11
|
+
* @param filePath The path of the file in the bucket
|
|
12
|
+
* @returns The signed URL
|
|
13
|
+
*/
|
|
14
|
+
export declare function getSignedUrl(filePath: string): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Deletes a file from Google Cloud Storage
|
|
17
|
+
* @param filePath The path of the file to delete
|
|
18
|
+
*/
|
|
19
|
+
export declare function deleteFile(filePath: string): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=googleCloudStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"googleCloudStorage.d.ts","sourceRoot":"","sources":["../../src/lib/googleCloudStorage.ts"],"names":[],"mappings":"AAgBA;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAepE;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhE"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.uploadFile = uploadFile;
|
|
4
|
+
exports.getSignedUrl = getSignedUrl;
|
|
5
|
+
exports.deleteFile = deleteFile;
|
|
6
|
+
const storage_1 = require("@google-cloud/storage");
|
|
7
|
+
const server_1 = require("@trpc/server");
|
|
8
|
+
const storage = new storage_1.Storage({
|
|
9
|
+
projectId: process.env.GOOGLE_CLOUD_PROJECT_ID,
|
|
10
|
+
credentials: {
|
|
11
|
+
client_email: process.env.GOOGLE_CLOUD_CLIENT_EMAIL,
|
|
12
|
+
private_key: process.env.GOOGLE_CLOUD_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
const bucket = storage.bucket(process.env.GOOGLE_CLOUD_BUCKET_NAME || '');
|
|
16
|
+
// Short expiration time for signed URLs (5 minutes)
|
|
17
|
+
const SIGNED_URL_EXPIRATION = 5 * 60 * 1000;
|
|
18
|
+
/**
|
|
19
|
+
* Uploads a file to Google Cloud Storage
|
|
20
|
+
* @param base64Data Base64 encoded file data
|
|
21
|
+
* @param filePath The path where the file should be stored
|
|
22
|
+
* @param contentType The MIME type of the file
|
|
23
|
+
* @returns The path of the uploaded file
|
|
24
|
+
*/
|
|
25
|
+
async function uploadFile(base64Data, filePath, contentType) {
|
|
26
|
+
try {
|
|
27
|
+
// Remove the data URL prefix if present
|
|
28
|
+
const base64Content = base64Data.includes('base64,')
|
|
29
|
+
? base64Data.split('base64,')[1]
|
|
30
|
+
: base64Data;
|
|
31
|
+
// Convert base64 to buffer
|
|
32
|
+
const fileBuffer = Buffer.from(base64Content, 'base64');
|
|
33
|
+
// Create a new file in the bucket
|
|
34
|
+
const file = bucket.file(filePath);
|
|
35
|
+
// Upload the file
|
|
36
|
+
await file.save(fileBuffer, {
|
|
37
|
+
metadata: {
|
|
38
|
+
contentType,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return filePath;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('Error uploading to Google Cloud Storage:', error);
|
|
45
|
+
throw new server_1.TRPCError({
|
|
46
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
47
|
+
message: 'Failed to upload file to storage',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Gets a signed URL for a file
|
|
53
|
+
* @param filePath The path of the file in the bucket
|
|
54
|
+
* @returns The signed URL
|
|
55
|
+
*/
|
|
56
|
+
async function getSignedUrl(filePath) {
|
|
57
|
+
try {
|
|
58
|
+
const [url] = await bucket.file(filePath).getSignedUrl({
|
|
59
|
+
version: 'v4',
|
|
60
|
+
action: 'read',
|
|
61
|
+
expires: Date.now() + SIGNED_URL_EXPIRATION,
|
|
62
|
+
});
|
|
63
|
+
return url;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error('Error getting signed URL:', error);
|
|
67
|
+
throw new server_1.TRPCError({
|
|
68
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
69
|
+
message: 'Failed to get signed URL',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Deletes a file from Google Cloud Storage
|
|
75
|
+
* @param filePath The path of the file to delete
|
|
76
|
+
*/
|
|
77
|
+
async function deleteFile(filePath) {
|
|
78
|
+
try {
|
|
79
|
+
await bucket.file(filePath).delete();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error('Error deleting file from Google Cloud Storage:', error);
|
|
83
|
+
throw new server_1.TRPCError({
|
|
84
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
85
|
+
message: 'Failed to delete file from storage',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client';
|
|
2
|
+
declare const prismaClientSingleton: () => PrismaClient<import(".prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
3
|
+
declare global {
|
|
4
|
+
var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
|
|
5
|
+
}
|
|
6
|
+
export declare const prisma: PrismaClient<import(".prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=prisma.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../src/lib/prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,QAAA,MAAM,qBAAqB,sIAE1B,CAAC;AAGF,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;CAClE;AAED,eAAO,MAAM,MAAM,gIAA+C,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prisma = void 0;
|
|
4
|
+
const client_1 = require("@prisma/client");
|
|
5
|
+
const prismaClientSingleton = () => {
|
|
6
|
+
return new client_1.PrismaClient();
|
|
7
|
+
};
|
|
8
|
+
exports.prisma = globalThis.prisma ?? prismaClientSingleton();
|
|
9
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
10
|
+
globalThis.prisma = exports.prisma;
|
|
11
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a thumbnail for an image or PDF file
|
|
3
|
+
* @param fileBuffer The file buffer
|
|
4
|
+
* @param fileType The MIME type of the file
|
|
5
|
+
* @returns Thumbnail buffer
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateMediaThumbnail(fileBuffer: Buffer, fileType: string): Promise<Buffer>;
|
|
8
|
+
/**
|
|
9
|
+
* Generates a thumbnail for a file
|
|
10
|
+
* @param fileName The name of the file in Google Cloud Storage
|
|
11
|
+
* @param fileType The MIME type of the file
|
|
12
|
+
* @returns The thumbnail buffer or null if thumbnail generation is not supported
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateThumbnail(fileName: string, fileType: string): Promise<Buffer | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Stores a thumbnail in Google Cloud Storage and creates a File entry
|
|
17
|
+
* @param thumbnailBuffer The thumbnail buffer to store
|
|
18
|
+
* @param originalFileName The original file name
|
|
19
|
+
* @param userId The user ID who owns the file
|
|
20
|
+
* @returns The ID of the created thumbnail File
|
|
21
|
+
*/
|
|
22
|
+
export declare function storeThumbnail(thumbnailBuffer: Buffer, originalFileName: string, userId: string): Promise<string>;
|
|
23
|
+
//# sourceMappingURL=thumbnailGenerator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnailGenerator.d.ts","sourceRoot":"","sources":["../../src/lib/thumbnailGenerator.ts"],"names":[],"mappings":"AAiDA;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BlG;AA4CD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA0BlG;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,eAAe,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBvH"}
|