@studious-lms/server 1.1.9 → 1.1.10
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/dist/index.js +68 -0
- package/dist/routers/_app.d.ts +68 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +10 -4
- package/dist/routers/message.d.ts +34 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +208 -2
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +5 -4
- package/package.json +1 -1
- package/src/index.ts +79 -0
- package/src/routers/conversation.ts +11 -4
- package/src/routers/message.ts +233 -2
- package/src/routers/user.ts +5 -3
package/dist/index.js
CHANGED
|
@@ -18,6 +18,8 @@ app.use(cors({
|
|
|
18
18
|
'http://localhost:3001', // Server port
|
|
19
19
|
'http://127.0.0.1:3000', // Alternative localhost
|
|
20
20
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
21
|
+
'https://www.studious.sh', // Production frontend
|
|
22
|
+
'https://studious.sh', // Production frontend (without www)
|
|
21
23
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
22
24
|
],
|
|
23
25
|
credentials: true,
|
|
@@ -32,6 +34,8 @@ app.options('*', (req, res) => {
|
|
|
32
34
|
'http://localhost:3001',
|
|
33
35
|
'http://127.0.0.1:3000',
|
|
34
36
|
'http://127.0.0.1:3001',
|
|
37
|
+
'https://www.studious.sh', // Production frontend
|
|
38
|
+
'https://studious.sh', // Production frontend (without www)
|
|
35
39
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
36
40
|
];
|
|
37
41
|
const origin = req.headers.origin;
|
|
@@ -82,6 +86,8 @@ const io = new Server(httpServer, {
|
|
|
82
86
|
'http://localhost:3001', // Server port
|
|
83
87
|
'http://127.0.0.1:3000', // Alternative localhost
|
|
84
88
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
89
|
+
'https://www.studious.sh', // Production frontend
|
|
90
|
+
'https://studious.sh', // Production frontend (without www)
|
|
85
91
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
86
92
|
],
|
|
87
93
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
@@ -137,6 +143,68 @@ app.get('/api/files/:filePath', async (req, res) => {
|
|
|
137
143
|
res.status(500).json({ error: 'Internal server error' });
|
|
138
144
|
}
|
|
139
145
|
});
|
|
146
|
+
// File upload endpoint for secure file uploads (supports both POST and PUT)
|
|
147
|
+
app.post('/api/upload/:filePath', async (req, res) => {
|
|
148
|
+
handleFileUpload(req, res);
|
|
149
|
+
});
|
|
150
|
+
app.put('/api/upload/:filePath', async (req, res) => {
|
|
151
|
+
handleFileUpload(req, res);
|
|
152
|
+
});
|
|
153
|
+
function handleFileUpload(req, res) {
|
|
154
|
+
try {
|
|
155
|
+
const filePath = decodeURIComponent(req.params.filePath);
|
|
156
|
+
console.log('File upload request:', { filePath, originalPath: req.params.filePath, method: req.method });
|
|
157
|
+
// Set CORS headers for upload endpoint
|
|
158
|
+
const origin = req.headers.origin;
|
|
159
|
+
const allowedOrigins = [
|
|
160
|
+
'http://localhost:3000',
|
|
161
|
+
'http://localhost:3001',
|
|
162
|
+
'http://127.0.0.1:3000',
|
|
163
|
+
'http://127.0.0.1:3001',
|
|
164
|
+
'https://www.studious.sh', // Production frontend
|
|
165
|
+
'https://studious.sh', // Production frontend (without www)
|
|
166
|
+
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
167
|
+
];
|
|
168
|
+
if (origin && allowedOrigins.includes(origin)) {
|
|
169
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
|
|
173
|
+
}
|
|
174
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
175
|
+
// Get content type from headers
|
|
176
|
+
const contentType = req.headers['content-type'] || 'application/octet-stream';
|
|
177
|
+
// Create a new file in the bucket
|
|
178
|
+
const file = bucket.file(filePath);
|
|
179
|
+
// Create a write stream to Google Cloud Storage
|
|
180
|
+
const writeStream = file.createWriteStream({
|
|
181
|
+
metadata: {
|
|
182
|
+
contentType,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
// Handle stream events
|
|
186
|
+
writeStream.on('error', (error) => {
|
|
187
|
+
console.error('Error uploading file:', error);
|
|
188
|
+
if (!res.headersSent) {
|
|
189
|
+
res.status(500).json({ error: 'Error uploading file' });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
writeStream.on('finish', () => {
|
|
193
|
+
console.log('File uploaded successfully:', filePath);
|
|
194
|
+
res.status(200).json({
|
|
195
|
+
success: true,
|
|
196
|
+
filePath,
|
|
197
|
+
message: 'File uploaded successfully'
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
// Pipe the request body to the write stream
|
|
201
|
+
req.pipe(writeStream);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.error('Error handling file upload:', error);
|
|
205
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
140
208
|
// Create caller
|
|
141
209
|
const createCaller = createCallerFactory(appRouter);
|
|
142
210
|
// Setup tRPC middleware
|
package/dist/routers/_app.d.ts
CHANGED
|
@@ -3548,6 +3548,40 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
3548
3548
|
};
|
|
3549
3549
|
meta: object;
|
|
3550
3550
|
}>;
|
|
3551
|
+
update: import("@trpc/server").TRPCMutationProcedure<{
|
|
3552
|
+
input: {
|
|
3553
|
+
content: string;
|
|
3554
|
+
messageId: string;
|
|
3555
|
+
mentionedUserIds?: string[] | undefined;
|
|
3556
|
+
};
|
|
3557
|
+
output: {
|
|
3558
|
+
id: string;
|
|
3559
|
+
content: string;
|
|
3560
|
+
senderId: string;
|
|
3561
|
+
conversationId: string;
|
|
3562
|
+
createdAt: Date;
|
|
3563
|
+
sender: {
|
|
3564
|
+
id: string;
|
|
3565
|
+
username: string;
|
|
3566
|
+
profile: {
|
|
3567
|
+
displayName: string | null;
|
|
3568
|
+
profilePicture: string | null;
|
|
3569
|
+
} | null;
|
|
3570
|
+
};
|
|
3571
|
+
mentionedUserIds: string[];
|
|
3572
|
+
};
|
|
3573
|
+
meta: object;
|
|
3574
|
+
}>;
|
|
3575
|
+
delete: import("@trpc/server").TRPCMutationProcedure<{
|
|
3576
|
+
input: {
|
|
3577
|
+
messageId: string;
|
|
3578
|
+
};
|
|
3579
|
+
output: {
|
|
3580
|
+
success: boolean;
|
|
3581
|
+
messageId: string;
|
|
3582
|
+
};
|
|
3583
|
+
meta: object;
|
|
3584
|
+
}>;
|
|
3551
3585
|
markAsRead: import("@trpc/server").TRPCMutationProcedure<{
|
|
3552
3586
|
input: {
|
|
3553
3587
|
conversationId: string;
|
|
@@ -7130,6 +7164,40 @@ export declare const createCaller: import("@trpc/server").TRPCRouterCaller<{
|
|
|
7130
7164
|
};
|
|
7131
7165
|
meta: object;
|
|
7132
7166
|
}>;
|
|
7167
|
+
update: import("@trpc/server").TRPCMutationProcedure<{
|
|
7168
|
+
input: {
|
|
7169
|
+
content: string;
|
|
7170
|
+
messageId: string;
|
|
7171
|
+
mentionedUserIds?: string[] | undefined;
|
|
7172
|
+
};
|
|
7173
|
+
output: {
|
|
7174
|
+
id: string;
|
|
7175
|
+
content: string;
|
|
7176
|
+
senderId: string;
|
|
7177
|
+
conversationId: string;
|
|
7178
|
+
createdAt: Date;
|
|
7179
|
+
sender: {
|
|
7180
|
+
id: string;
|
|
7181
|
+
username: string;
|
|
7182
|
+
profile: {
|
|
7183
|
+
displayName: string | null;
|
|
7184
|
+
profilePicture: string | null;
|
|
7185
|
+
} | null;
|
|
7186
|
+
};
|
|
7187
|
+
mentionedUserIds: string[];
|
|
7188
|
+
};
|
|
7189
|
+
meta: object;
|
|
7190
|
+
}>;
|
|
7191
|
+
delete: import("@trpc/server").TRPCMutationProcedure<{
|
|
7192
|
+
input: {
|
|
7193
|
+
messageId: string;
|
|
7194
|
+
};
|
|
7195
|
+
output: {
|
|
7196
|
+
success: boolean;
|
|
7197
|
+
messageId: string;
|
|
7198
|
+
};
|
|
7199
|
+
meta: object;
|
|
7200
|
+
}>;
|
|
7133
7201
|
markAsRead: import("@trpc/server").TRPCMutationProcedure<{
|
|
7134
7202
|
input: {
|
|
7135
7203
|
conversationId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_app.d.ts","sourceRoot":"","sources":["../../src/routers/_app.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAY1E,eAAO,MAAM,SAAS
|
|
1
|
+
{"version":3,"file":"_app.d.ts","sourceRoot":"","sources":["../../src/routers/_app.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAY1E,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAepB,CAAC;AAGH,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC;AACzC,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;AAG1D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAAiC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/routers/conversation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/routers/conversation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8R7B,CAAC"}
|
|
@@ -70,14 +70,19 @@ export const conversationRouter = createTRPCRouter({
|
|
|
70
70
|
},
|
|
71
71
|
});
|
|
72
72
|
// Count unread mentions
|
|
73
|
+
// Use the later of lastViewedAt or lastViewedMentionAt
|
|
74
|
+
// This means if user viewed conversation after mention, mention is considered read
|
|
75
|
+
const mentionCutoffTime = lastViewedMentionAt && lastViewedAt
|
|
76
|
+
? (lastViewedMentionAt > lastViewedAt ? lastViewedMentionAt : lastViewedAt)
|
|
77
|
+
: (lastViewedMentionAt || lastViewedAt);
|
|
73
78
|
const unreadMentionCount = await prisma.mention.count({
|
|
74
79
|
where: {
|
|
75
80
|
userId,
|
|
76
81
|
message: {
|
|
77
82
|
conversationId: conversation.id,
|
|
78
83
|
senderId: { not: userId },
|
|
79
|
-
...(
|
|
80
|
-
createdAt: { gt:
|
|
84
|
+
...(mentionCutoffTime && {
|
|
85
|
+
createdAt: { gt: mentionCutoffTime }
|
|
81
86
|
}),
|
|
82
87
|
},
|
|
83
88
|
},
|
|
@@ -164,12 +169,13 @@ export const conversationRouter = createTRPCRouter({
|
|
|
164
169
|
// Verify all members exist
|
|
165
170
|
const members = await prisma.user.findMany({
|
|
166
171
|
where: {
|
|
167
|
-
|
|
172
|
+
username: {
|
|
168
173
|
in: memberIds,
|
|
169
174
|
},
|
|
170
175
|
},
|
|
171
176
|
select: {
|
|
172
177
|
id: true,
|
|
178
|
+
username: true,
|
|
173
179
|
},
|
|
174
180
|
});
|
|
175
181
|
if (members.length !== memberIds.length) {
|
|
@@ -190,7 +196,7 @@ export const conversationRouter = createTRPCRouter({
|
|
|
190
196
|
role: type === 'GROUP' ? 'ADMIN' : 'MEMBER',
|
|
191
197
|
},
|
|
192
198
|
...memberIds.map((memberId) => ({
|
|
193
|
-
userId: memberId,
|
|
199
|
+
userId: members.find((member) => member.username === memberId).id,
|
|
194
200
|
role: 'MEMBER',
|
|
195
201
|
})),
|
|
196
202
|
],
|
|
@@ -76,6 +76,40 @@ export declare const messageRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
76
76
|
};
|
|
77
77
|
meta: object;
|
|
78
78
|
}>;
|
|
79
|
+
update: import("@trpc/server").TRPCMutationProcedure<{
|
|
80
|
+
input: {
|
|
81
|
+
content: string;
|
|
82
|
+
messageId: string;
|
|
83
|
+
mentionedUserIds?: string[] | undefined;
|
|
84
|
+
};
|
|
85
|
+
output: {
|
|
86
|
+
id: string;
|
|
87
|
+
content: string;
|
|
88
|
+
senderId: string;
|
|
89
|
+
conversationId: string;
|
|
90
|
+
createdAt: Date;
|
|
91
|
+
sender: {
|
|
92
|
+
id: string;
|
|
93
|
+
username: string;
|
|
94
|
+
profile: {
|
|
95
|
+
displayName: string | null;
|
|
96
|
+
profilePicture: string | null;
|
|
97
|
+
} | null;
|
|
98
|
+
};
|
|
99
|
+
mentionedUserIds: string[];
|
|
100
|
+
};
|
|
101
|
+
meta: object;
|
|
102
|
+
}>;
|
|
103
|
+
delete: import("@trpc/server").TRPCMutationProcedure<{
|
|
104
|
+
input: {
|
|
105
|
+
messageId: string;
|
|
106
|
+
};
|
|
107
|
+
output: {
|
|
108
|
+
success: boolean;
|
|
109
|
+
messageId: string;
|
|
110
|
+
};
|
|
111
|
+
meta: object;
|
|
112
|
+
}>;
|
|
79
113
|
markAsRead: import("@trpc/server").TRPCMutationProcedure<{
|
|
80
114
|
input: {
|
|
81
115
|
conversationId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/routers/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/routers/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6kBxB,CAAC"}
|
package/dist/routers/message.js
CHANGED
|
@@ -192,6 +192,207 @@ export const messageRouter = createTRPCRouter({
|
|
|
192
192
|
mentionedUserIds,
|
|
193
193
|
};
|
|
194
194
|
}),
|
|
195
|
+
update: protectedProcedure
|
|
196
|
+
.input(z.object({
|
|
197
|
+
messageId: z.string(),
|
|
198
|
+
content: z.string().min(1).max(4000),
|
|
199
|
+
mentionedUserIds: z.array(z.string()).optional(),
|
|
200
|
+
}))
|
|
201
|
+
.mutation(async ({ input, ctx }) => {
|
|
202
|
+
const userId = ctx.user.id;
|
|
203
|
+
const { messageId, content, mentionedUserIds = [] } = input;
|
|
204
|
+
// Get the existing message and verify user is the sender
|
|
205
|
+
const existingMessage = await prisma.message.findUnique({
|
|
206
|
+
where: { id: messageId },
|
|
207
|
+
include: {
|
|
208
|
+
sender: {
|
|
209
|
+
select: {
|
|
210
|
+
id: true,
|
|
211
|
+
username: true,
|
|
212
|
+
profile: {
|
|
213
|
+
select: {
|
|
214
|
+
displayName: true,
|
|
215
|
+
profilePicture: true,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
if (!existingMessage) {
|
|
223
|
+
throw new TRPCError({
|
|
224
|
+
code: 'NOT_FOUND',
|
|
225
|
+
message: 'Message not found',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (existingMessage.senderId !== userId) {
|
|
229
|
+
throw new TRPCError({
|
|
230
|
+
code: 'FORBIDDEN',
|
|
231
|
+
message: 'Not the sender of this message',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// Verify user is still a member of the conversation
|
|
235
|
+
const membership = await prisma.conversationMember.findFirst({
|
|
236
|
+
where: {
|
|
237
|
+
conversationId: existingMessage.conversationId,
|
|
238
|
+
userId,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
if (!membership) {
|
|
242
|
+
throw new TRPCError({
|
|
243
|
+
code: 'FORBIDDEN',
|
|
244
|
+
message: 'Not a member of this conversation',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// Verify mentioned users are members of the conversation
|
|
248
|
+
if (mentionedUserIds.length > 0) {
|
|
249
|
+
const mentionedMemberships = await prisma.conversationMember.findMany({
|
|
250
|
+
where: {
|
|
251
|
+
conversationId: existingMessage.conversationId,
|
|
252
|
+
userId: { in: mentionedUserIds },
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
if (mentionedMemberships.length !== mentionedUserIds.length) {
|
|
256
|
+
throw new TRPCError({
|
|
257
|
+
code: 'BAD_REQUEST',
|
|
258
|
+
message: 'Some mentioned users are not members of this conversation',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Update message and mentions in transaction
|
|
263
|
+
const updatedMessage = await prisma.$transaction(async (tx) => {
|
|
264
|
+
// Update the message content
|
|
265
|
+
const message = await tx.message.update({
|
|
266
|
+
where: { id: messageId },
|
|
267
|
+
data: { content },
|
|
268
|
+
include: {
|
|
269
|
+
sender: {
|
|
270
|
+
select: {
|
|
271
|
+
id: true,
|
|
272
|
+
username: true,
|
|
273
|
+
profile: {
|
|
274
|
+
select: {
|
|
275
|
+
displayName: true,
|
|
276
|
+
profilePicture: true,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
// Delete existing mentions
|
|
284
|
+
await tx.mention.deleteMany({
|
|
285
|
+
where: { messageId },
|
|
286
|
+
});
|
|
287
|
+
// Create new mentions if any
|
|
288
|
+
if (mentionedUserIds.length > 0) {
|
|
289
|
+
await tx.mention.createMany({
|
|
290
|
+
data: mentionedUserIds.map((mentionedUserId) => ({
|
|
291
|
+
messageId,
|
|
292
|
+
userId: mentionedUserId,
|
|
293
|
+
})),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return message;
|
|
297
|
+
});
|
|
298
|
+
// Broadcast message update to Pusher
|
|
299
|
+
try {
|
|
300
|
+
await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-updated', {
|
|
301
|
+
id: updatedMessage.id,
|
|
302
|
+
content: updatedMessage.content,
|
|
303
|
+
senderId: updatedMessage.senderId,
|
|
304
|
+
conversationId: updatedMessage.conversationId,
|
|
305
|
+
createdAt: updatedMessage.createdAt,
|
|
306
|
+
sender: updatedMessage.sender,
|
|
307
|
+
mentionedUserIds,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
console.error('Failed to broadcast message update:', error);
|
|
312
|
+
// Don't fail the request if Pusher fails
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
id: updatedMessage.id,
|
|
316
|
+
content: updatedMessage.content,
|
|
317
|
+
senderId: updatedMessage.senderId,
|
|
318
|
+
conversationId: updatedMessage.conversationId,
|
|
319
|
+
createdAt: updatedMessage.createdAt,
|
|
320
|
+
sender: updatedMessage.sender,
|
|
321
|
+
mentionedUserIds,
|
|
322
|
+
};
|
|
323
|
+
}),
|
|
324
|
+
delete: protectedProcedure
|
|
325
|
+
.input(z.object({
|
|
326
|
+
messageId: z.string(),
|
|
327
|
+
}))
|
|
328
|
+
.mutation(async ({ input, ctx }) => {
|
|
329
|
+
const userId = ctx.user.id;
|
|
330
|
+
const { messageId } = input;
|
|
331
|
+
// Get the message and verify user is the sender
|
|
332
|
+
const existingMessage = await prisma.message.findUnique({
|
|
333
|
+
where: { id: messageId },
|
|
334
|
+
include: {
|
|
335
|
+
sender: {
|
|
336
|
+
select: {
|
|
337
|
+
id: true,
|
|
338
|
+
username: true,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
if (!existingMessage) {
|
|
344
|
+
throw new TRPCError({
|
|
345
|
+
code: 'NOT_FOUND',
|
|
346
|
+
message: 'Message not found',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (existingMessage.senderId !== userId) {
|
|
350
|
+
throw new TRPCError({
|
|
351
|
+
code: 'FORBIDDEN',
|
|
352
|
+
message: 'Not the sender of this message',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
// Verify user is still a member of the conversation
|
|
356
|
+
const membership = await prisma.conversationMember.findFirst({
|
|
357
|
+
where: {
|
|
358
|
+
conversationId: existingMessage.conversationId,
|
|
359
|
+
userId,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
if (!membership) {
|
|
363
|
+
throw new TRPCError({
|
|
364
|
+
code: 'FORBIDDEN',
|
|
365
|
+
message: 'Not a member of this conversation',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
// Delete message and all related mentions in transaction
|
|
369
|
+
await prisma.$transaction(async (tx) => {
|
|
370
|
+
// Delete mentions first (due to foreign key constraint)
|
|
371
|
+
await tx.mention.deleteMany({
|
|
372
|
+
where: { messageId },
|
|
373
|
+
});
|
|
374
|
+
// Delete the message
|
|
375
|
+
await tx.message.delete({
|
|
376
|
+
where: { id: messageId },
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
// Broadcast message deletion to Pusher
|
|
380
|
+
try {
|
|
381
|
+
await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-deleted', {
|
|
382
|
+
messageId,
|
|
383
|
+
conversationId: existingMessage.conversationId,
|
|
384
|
+
senderId: existingMessage.senderId,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
console.error('Failed to broadcast message deletion:', error);
|
|
389
|
+
// Don't fail the request if Pusher fails
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
messageId,
|
|
394
|
+
};
|
|
395
|
+
}),
|
|
195
396
|
markAsRead: protectedProcedure
|
|
196
397
|
.input(z.object({
|
|
197
398
|
conversationId: z.string(),
|
|
@@ -305,14 +506,19 @@ export const messageRouter = createTRPCRouter({
|
|
|
305
506
|
},
|
|
306
507
|
});
|
|
307
508
|
// Count unread mentions
|
|
509
|
+
// Use the later of lastViewedAt or lastViewedMentionAt
|
|
510
|
+
// This means if user viewed conversation after mention, mention is considered read
|
|
511
|
+
const mentionCutoffTime = membership.lastViewedMentionAt && membership.lastViewedAt
|
|
512
|
+
? (membership.lastViewedMentionAt > membership.lastViewedAt ? membership.lastViewedMentionAt : membership.lastViewedAt)
|
|
513
|
+
: (membership.lastViewedMentionAt || membership.lastViewedAt);
|
|
308
514
|
const unreadMentionCount = await prisma.mention.count({
|
|
309
515
|
where: {
|
|
310
516
|
userId,
|
|
311
517
|
message: {
|
|
312
518
|
conversationId,
|
|
313
519
|
senderId: { not: userId },
|
|
314
|
-
...(
|
|
315
|
-
createdAt: { gt:
|
|
520
|
+
...(mentionCutoffTime && {
|
|
521
|
+
createdAt: { gt: mentionCutoffTime }
|
|
316
522
|
}),
|
|
317
523
|
},
|
|
318
524
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2DxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2DxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuMrB,CAAC"}
|
package/dist/routers/user.js
CHANGED
|
@@ -2,7 +2,6 @@ import { z } from "zod";
|
|
|
2
2
|
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
3
3
|
import { TRPCError } from "@trpc/server";
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
|
-
import { getSignedUrl } from "../lib/googleCloudStorage.js";
|
|
6
5
|
import { logger } from "../utils/logger.js";
|
|
7
6
|
// Helper function to convert file path to backend proxy URL
|
|
8
7
|
function getFileUrl(filePath) {
|
|
@@ -203,13 +202,15 @@ export const userRouter = createTRPCRouter({
|
|
|
203
202
|
const fileExtension = input.fileName.split('.').pop();
|
|
204
203
|
const uniqueFilename = `${ctx.user.id}-${Date.now()}.${fileExtension}`;
|
|
205
204
|
const filePath = `users/${ctx.user.id}/profile/${uniqueFilename}`;
|
|
206
|
-
// Generate
|
|
207
|
-
const
|
|
205
|
+
// Generate backend proxy upload URL instead of direct GCS signed URL
|
|
206
|
+
const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
|
|
207
|
+
const uploadUrl = `${backendUrl}/api/upload/${encodeURIComponent(filePath)}`;
|
|
208
208
|
logger.info('Generated upload URL', {
|
|
209
209
|
userId: ctx.user.id,
|
|
210
210
|
filePath,
|
|
211
211
|
fileName: uniqueFilename,
|
|
212
|
-
fileType: input.fileType
|
|
212
|
+
fileType: input.fileType,
|
|
213
|
+
uploadUrl
|
|
213
214
|
});
|
|
214
215
|
return {
|
|
215
216
|
uploadUrl,
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ app.use(cors({
|
|
|
22
22
|
'http://localhost:3001', // Server port
|
|
23
23
|
'http://127.0.0.1:3000', // Alternative localhost
|
|
24
24
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
25
|
+
'https://www.studious.sh', // Production frontend
|
|
26
|
+
'https://studious.sh', // Production frontend (without www)
|
|
25
27
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
26
28
|
],
|
|
27
29
|
credentials: true,
|
|
@@ -37,6 +39,8 @@ app.options('*', (req, res) => {
|
|
|
37
39
|
'http://localhost:3001',
|
|
38
40
|
'http://127.0.0.1:3000',
|
|
39
41
|
'http://127.0.0.1:3001',
|
|
42
|
+
'https://www.studious.sh', // Production frontend
|
|
43
|
+
'https://studious.sh', // Production frontend (without www)
|
|
40
44
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
41
45
|
];
|
|
42
46
|
|
|
@@ -92,6 +96,8 @@ const io = new Server(httpServer, {
|
|
|
92
96
|
'http://localhost:3001', // Server port
|
|
93
97
|
'http://127.0.0.1:3000', // Alternative localhost
|
|
94
98
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
99
|
+
'https://www.studious.sh', // Production frontend
|
|
100
|
+
'https://studious.sh', // Production frontend (without www)
|
|
95
101
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
96
102
|
],
|
|
97
103
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
@@ -158,6 +164,79 @@ app.get('/api/files/:filePath', async (req, res) => {
|
|
|
158
164
|
}
|
|
159
165
|
});
|
|
160
166
|
|
|
167
|
+
// File upload endpoint for secure file uploads (supports both POST and PUT)
|
|
168
|
+
app.post('/api/upload/:filePath', async (req, res) => {
|
|
169
|
+
handleFileUpload(req, res);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
app.put('/api/upload/:filePath', async (req, res) => {
|
|
173
|
+
handleFileUpload(req, res);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
function handleFileUpload(req: any, res: any) {
|
|
177
|
+
try {
|
|
178
|
+
const filePath = decodeURIComponent(req.params.filePath);
|
|
179
|
+
console.log('File upload request:', { filePath, originalPath: req.params.filePath, method: req.method });
|
|
180
|
+
|
|
181
|
+
// Set CORS headers for upload endpoint
|
|
182
|
+
const origin = req.headers.origin;
|
|
183
|
+
const allowedOrigins = [
|
|
184
|
+
'http://localhost:3000',
|
|
185
|
+
'http://localhost:3001',
|
|
186
|
+
'http://127.0.0.1:3000',
|
|
187
|
+
'http://127.0.0.1:3001',
|
|
188
|
+
'https://www.studious.sh', // Production frontend
|
|
189
|
+
'https://studious.sh', // Production frontend (without www)
|
|
190
|
+
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
if (origin && allowedOrigins.includes(origin)) {
|
|
194
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
195
|
+
} else {
|
|
196
|
+
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
200
|
+
|
|
201
|
+
// Get content type from headers
|
|
202
|
+
const contentType = req.headers['content-type'] || 'application/octet-stream';
|
|
203
|
+
|
|
204
|
+
// Create a new file in the bucket
|
|
205
|
+
const file = bucket.file(filePath);
|
|
206
|
+
|
|
207
|
+
// Create a write stream to Google Cloud Storage
|
|
208
|
+
const writeStream = file.createWriteStream({
|
|
209
|
+
metadata: {
|
|
210
|
+
contentType,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Handle stream events
|
|
215
|
+
writeStream.on('error', (error) => {
|
|
216
|
+
console.error('Error uploading file:', error);
|
|
217
|
+
if (!res.headersSent) {
|
|
218
|
+
res.status(500).json({ error: 'Error uploading file' });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
writeStream.on('finish', () => {
|
|
223
|
+
console.log('File uploaded successfully:', filePath);
|
|
224
|
+
res.status(200).json({
|
|
225
|
+
success: true,
|
|
226
|
+
filePath,
|
|
227
|
+
message: 'File uploaded successfully'
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Pipe the request body to the write stream
|
|
232
|
+
req.pipe(writeStream);
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('Error handling file upload:', error);
|
|
236
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
161
240
|
// Create caller
|
|
162
241
|
const createCaller = createCallerFactory(appRouter);
|
|
163
242
|
|
|
@@ -76,14 +76,20 @@ export const conversationRouter = createTRPCRouter({
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
// Count unread mentions
|
|
79
|
+
// Use the later of lastViewedAt or lastViewedMentionAt
|
|
80
|
+
// This means if user viewed conversation after mention, mention is considered read
|
|
81
|
+
const mentionCutoffTime = lastViewedMentionAt && lastViewedAt
|
|
82
|
+
? (lastViewedMentionAt > lastViewedAt ? lastViewedMentionAt : lastViewedAt)
|
|
83
|
+
: (lastViewedMentionAt || lastViewedAt);
|
|
84
|
+
|
|
79
85
|
const unreadMentionCount = await prisma.mention.count({
|
|
80
86
|
where: {
|
|
81
87
|
userId,
|
|
82
88
|
message: {
|
|
83
89
|
conversationId: conversation.id,
|
|
84
90
|
senderId: { not: userId },
|
|
85
|
-
...(
|
|
86
|
-
createdAt: { gt:
|
|
91
|
+
...(mentionCutoffTime && {
|
|
92
|
+
createdAt: { gt: mentionCutoffTime }
|
|
87
93
|
}),
|
|
88
94
|
},
|
|
89
95
|
},
|
|
@@ -181,12 +187,13 @@ export const conversationRouter = createTRPCRouter({
|
|
|
181
187
|
// Verify all members exist
|
|
182
188
|
const members = await prisma.user.findMany({
|
|
183
189
|
where: {
|
|
184
|
-
|
|
190
|
+
username: {
|
|
185
191
|
in: memberIds,
|
|
186
192
|
},
|
|
187
193
|
},
|
|
188
194
|
select: {
|
|
189
195
|
id: true,
|
|
196
|
+
username: true,
|
|
190
197
|
},
|
|
191
198
|
});
|
|
192
199
|
|
|
@@ -209,7 +216,7 @@ export const conversationRouter = createTRPCRouter({
|
|
|
209
216
|
role: type === 'GROUP' ? 'ADMIN' : 'MEMBER',
|
|
210
217
|
},
|
|
211
218
|
...memberIds.map((memberId) => ({
|
|
212
|
-
userId: memberId,
|
|
219
|
+
userId: members.find((member) => member.username === memberId)!.id,
|
|
213
220
|
role: 'MEMBER' as const,
|
|
214
221
|
})),
|
|
215
222
|
],
|
package/src/routers/message.ts
CHANGED
|
@@ -212,7 +212,232 @@ export const messageRouter = createTRPCRouter({
|
|
|
212
212
|
mentionedUserIds,
|
|
213
213
|
};
|
|
214
214
|
}),
|
|
215
|
+
update: protectedProcedure
|
|
216
|
+
.input(
|
|
217
|
+
z.object({
|
|
218
|
+
messageId: z.string(),
|
|
219
|
+
content: z.string().min(1).max(4000),
|
|
220
|
+
mentionedUserIds: z.array(z.string()).optional(),
|
|
221
|
+
})
|
|
222
|
+
)
|
|
223
|
+
.mutation(async ({ input, ctx }) => {
|
|
224
|
+
const userId = ctx.user!.id;
|
|
225
|
+
const { messageId, content, mentionedUserIds = [] } = input;
|
|
226
|
+
|
|
227
|
+
// Get the existing message and verify user is the sender
|
|
228
|
+
const existingMessage = await prisma.message.findUnique({
|
|
229
|
+
where: { id: messageId },
|
|
230
|
+
include: {
|
|
231
|
+
sender: {
|
|
232
|
+
select: {
|
|
233
|
+
id: true,
|
|
234
|
+
username: true,
|
|
235
|
+
profile: {
|
|
236
|
+
select: {
|
|
237
|
+
displayName: true,
|
|
238
|
+
profilePicture: true,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!existingMessage) {
|
|
247
|
+
throw new TRPCError({
|
|
248
|
+
code: 'NOT_FOUND',
|
|
249
|
+
message: 'Message not found',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (existingMessage.senderId !== userId) {
|
|
254
|
+
throw new TRPCError({
|
|
255
|
+
code: 'FORBIDDEN',
|
|
256
|
+
message: 'Not the sender of this message',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Verify user is still a member of the conversation
|
|
261
|
+
const membership = await prisma.conversationMember.findFirst({
|
|
262
|
+
where: {
|
|
263
|
+
conversationId: existingMessage.conversationId,
|
|
264
|
+
userId,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (!membership) {
|
|
269
|
+
throw new TRPCError({
|
|
270
|
+
code: 'FORBIDDEN',
|
|
271
|
+
message: 'Not a member of this conversation',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Verify mentioned users are members of the conversation
|
|
276
|
+
if (mentionedUserIds.length > 0) {
|
|
277
|
+
const mentionedMemberships = await prisma.conversationMember.findMany({
|
|
278
|
+
where: {
|
|
279
|
+
conversationId: existingMessage.conversationId,
|
|
280
|
+
userId: { in: mentionedUserIds },
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (mentionedMemberships.length !== mentionedUserIds.length) {
|
|
285
|
+
throw new TRPCError({
|
|
286
|
+
code: 'BAD_REQUEST',
|
|
287
|
+
message: 'Some mentioned users are not members of this conversation',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Update message and mentions in transaction
|
|
293
|
+
const updatedMessage = await prisma.$transaction(async (tx) => {
|
|
294
|
+
// Update the message content
|
|
295
|
+
const message = await tx.message.update({
|
|
296
|
+
where: { id: messageId },
|
|
297
|
+
data: { content },
|
|
298
|
+
include: {
|
|
299
|
+
sender: {
|
|
300
|
+
select: {
|
|
301
|
+
id: true,
|
|
302
|
+
username: true,
|
|
303
|
+
profile: {
|
|
304
|
+
select: {
|
|
305
|
+
displayName: true,
|
|
306
|
+
profilePicture: true,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
});
|
|
215
313
|
|
|
314
|
+
// Delete existing mentions
|
|
315
|
+
await tx.mention.deleteMany({
|
|
316
|
+
where: { messageId },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Create new mentions if any
|
|
320
|
+
if (mentionedUserIds.length > 0) {
|
|
321
|
+
await tx.mention.createMany({
|
|
322
|
+
data: mentionedUserIds.map((mentionedUserId) => ({
|
|
323
|
+
messageId,
|
|
324
|
+
userId: mentionedUserId,
|
|
325
|
+
})),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return message;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Broadcast message update to Pusher
|
|
333
|
+
try {
|
|
334
|
+
await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-updated', {
|
|
335
|
+
id: updatedMessage.id,
|
|
336
|
+
content: updatedMessage.content,
|
|
337
|
+
senderId: updatedMessage.senderId,
|
|
338
|
+
conversationId: updatedMessage.conversationId,
|
|
339
|
+
createdAt: updatedMessage.createdAt,
|
|
340
|
+
sender: updatedMessage.sender,
|
|
341
|
+
mentionedUserIds,
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('Failed to broadcast message update:', error);
|
|
345
|
+
// Don't fail the request if Pusher fails
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
id: updatedMessage.id,
|
|
350
|
+
content: updatedMessage.content,
|
|
351
|
+
senderId: updatedMessage.senderId,
|
|
352
|
+
conversationId: updatedMessage.conversationId,
|
|
353
|
+
createdAt: updatedMessage.createdAt,
|
|
354
|
+
sender: updatedMessage.sender,
|
|
355
|
+
mentionedUserIds,
|
|
356
|
+
};
|
|
357
|
+
}),
|
|
358
|
+
|
|
359
|
+
delete: protectedProcedure
|
|
360
|
+
.input(
|
|
361
|
+
z.object({
|
|
362
|
+
messageId: z.string(),
|
|
363
|
+
})
|
|
364
|
+
)
|
|
365
|
+
.mutation(async ({ input, ctx }) => {
|
|
366
|
+
const userId = ctx.user!.id;
|
|
367
|
+
const { messageId } = input;
|
|
368
|
+
|
|
369
|
+
// Get the message and verify user is the sender
|
|
370
|
+
const existingMessage = await prisma.message.findUnique({
|
|
371
|
+
where: { id: messageId },
|
|
372
|
+
include: {
|
|
373
|
+
sender: {
|
|
374
|
+
select: {
|
|
375
|
+
id: true,
|
|
376
|
+
username: true,
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (!existingMessage) {
|
|
383
|
+
throw new TRPCError({
|
|
384
|
+
code: 'NOT_FOUND',
|
|
385
|
+
message: 'Message not found',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (existingMessage.senderId !== userId) {
|
|
390
|
+
throw new TRPCError({
|
|
391
|
+
code: 'FORBIDDEN',
|
|
392
|
+
message: 'Not the sender of this message',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Verify user is still a member of the conversation
|
|
397
|
+
const membership = await prisma.conversationMember.findFirst({
|
|
398
|
+
where: {
|
|
399
|
+
conversationId: existingMessage.conversationId,
|
|
400
|
+
userId,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (!membership) {
|
|
405
|
+
throw new TRPCError({
|
|
406
|
+
code: 'FORBIDDEN',
|
|
407
|
+
message: 'Not a member of this conversation',
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Delete message and all related mentions in transaction
|
|
412
|
+
await prisma.$transaction(async (tx) => {
|
|
413
|
+
// Delete mentions first (due to foreign key constraint)
|
|
414
|
+
await tx.mention.deleteMany({
|
|
415
|
+
where: { messageId },
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Delete the message
|
|
419
|
+
await tx.message.delete({
|
|
420
|
+
where: { id: messageId },
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Broadcast message deletion to Pusher
|
|
425
|
+
try {
|
|
426
|
+
await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-deleted', {
|
|
427
|
+
messageId,
|
|
428
|
+
conversationId: existingMessage.conversationId,
|
|
429
|
+
senderId: existingMessage.senderId,
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.error('Failed to broadcast message deletion:', error);
|
|
433
|
+
// Don't fail the request if Pusher fails
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
success: true,
|
|
438
|
+
messageId,
|
|
439
|
+
};
|
|
440
|
+
}),
|
|
216
441
|
markAsRead: protectedProcedure
|
|
217
442
|
.input(
|
|
218
443
|
z.object({
|
|
@@ -344,14 +569,20 @@ export const messageRouter = createTRPCRouter({
|
|
|
344
569
|
});
|
|
345
570
|
|
|
346
571
|
// Count unread mentions
|
|
572
|
+
// Use the later of lastViewedAt or lastViewedMentionAt
|
|
573
|
+
// This means if user viewed conversation after mention, mention is considered read
|
|
574
|
+
const mentionCutoffTime = membership.lastViewedMentionAt && membership.lastViewedAt
|
|
575
|
+
? (membership.lastViewedMentionAt > membership.lastViewedAt ? membership.lastViewedMentionAt : membership.lastViewedAt)
|
|
576
|
+
: (membership.lastViewedMentionAt || membership.lastViewedAt);
|
|
577
|
+
|
|
347
578
|
const unreadMentionCount = await prisma.mention.count({
|
|
348
579
|
where: {
|
|
349
580
|
userId,
|
|
350
581
|
message: {
|
|
351
582
|
conversationId,
|
|
352
583
|
senderId: { not: userId },
|
|
353
|
-
...(
|
|
354
|
-
createdAt: { gt:
|
|
584
|
+
...(mentionCutoffTime && {
|
|
585
|
+
createdAt: { gt: mentionCutoffTime }
|
|
355
586
|
}),
|
|
356
587
|
},
|
|
357
588
|
},
|
package/src/routers/user.ts
CHANGED
|
@@ -228,14 +228,16 @@ export const userRouter = createTRPCRouter({
|
|
|
228
228
|
const uniqueFilename = `${ctx.user.id}-${Date.now()}.${fileExtension}`;
|
|
229
229
|
const filePath = `users/${ctx.user.id}/profile/${uniqueFilename}`;
|
|
230
230
|
|
|
231
|
-
// Generate
|
|
232
|
-
const
|
|
231
|
+
// Generate backend proxy upload URL instead of direct GCS signed URL
|
|
232
|
+
const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
|
|
233
|
+
const uploadUrl = `${backendUrl}/api/upload/${encodeURIComponent(filePath)}`;
|
|
233
234
|
|
|
234
235
|
logger.info('Generated upload URL', {
|
|
235
236
|
userId: ctx.user.id,
|
|
236
237
|
filePath,
|
|
237
238
|
fileName: uniqueFilename,
|
|
238
|
-
fileType: input.fileType
|
|
239
|
+
fileType: input.fileType,
|
|
240
|
+
uploadUrl
|
|
239
241
|
});
|
|
240
242
|
|
|
241
243
|
return {
|