@goscribe/server 1.1.3 → 1.1.5
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/routers/_app.d.ts +21 -11
- package/dist/routers/auth.d.ts +19 -2
- package/dist/routers/auth.js +46 -2
- package/dist/routers/chat.d.ts +0 -5
- package/dist/routers/chat.js +7 -10
- package/dist/routers/members.d.ts +0 -2
- package/dist/routers/members.js +14 -4
- package/dist/routers/workspace.d.ts +2 -4
- package/dist/server.js +12 -0
- package/package.json +2 -1
- package/prisma/schema.prisma +191 -189
- package/src/routers/auth.ts +50 -2
- package/src/routers/chat.ts +7 -10
- package/src/routers/members.ts +14 -4
- package/src/server.ts +13 -0
package/dist/routers/_app.d.ts
CHANGED
|
@@ -43,6 +43,25 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
43
43
|
};
|
|
44
44
|
transformer: true;
|
|
45
45
|
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
46
|
+
updateProfile: import("@trpc/server").TRPCMutationProcedure<{
|
|
47
|
+
input: {
|
|
48
|
+
name: string;
|
|
49
|
+
};
|
|
50
|
+
output: {
|
|
51
|
+
success: boolean;
|
|
52
|
+
message: string;
|
|
53
|
+
};
|
|
54
|
+
meta: object;
|
|
55
|
+
}>;
|
|
56
|
+
uploadProfilePicture: import("@trpc/server").TRPCMutationProcedure<{
|
|
57
|
+
input: void;
|
|
58
|
+
output: {
|
|
59
|
+
success: boolean;
|
|
60
|
+
message: string;
|
|
61
|
+
signedUrl: string;
|
|
62
|
+
};
|
|
63
|
+
meta: object;
|
|
64
|
+
}>;
|
|
46
65
|
signup: import("@trpc/server").TRPCMutationProcedure<{
|
|
47
66
|
input: {
|
|
48
67
|
name: string;
|
|
@@ -65,7 +84,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
65
84
|
id: string;
|
|
66
85
|
email: string | null;
|
|
67
86
|
name: string | null;
|
|
68
|
-
image: string | null;
|
|
69
87
|
token: string;
|
|
70
88
|
};
|
|
71
89
|
meta: object;
|
|
@@ -77,7 +95,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
77
95
|
id: string;
|
|
78
96
|
email: string | null;
|
|
79
97
|
name: string | null;
|
|
80
|
-
image: string | null;
|
|
81
98
|
};
|
|
82
99
|
};
|
|
83
100
|
meta: object;
|
|
@@ -233,8 +250,8 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
233
250
|
name: string;
|
|
234
251
|
id: string;
|
|
235
252
|
createdAt: Date;
|
|
236
|
-
userId: string;
|
|
237
|
-
workspaceId: string;
|
|
253
|
+
userId: string | null;
|
|
254
|
+
workspaceId: string | null;
|
|
238
255
|
mimeType: string;
|
|
239
256
|
size: number;
|
|
240
257
|
bucket: string | null;
|
|
@@ -499,14 +516,12 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
499
516
|
id: string;
|
|
500
517
|
name: string;
|
|
501
518
|
email: string;
|
|
502
|
-
image: string | null;
|
|
503
519
|
role: "admin" | "member";
|
|
504
520
|
joinedAt: Date;
|
|
505
521
|
} | {
|
|
506
522
|
id: string;
|
|
507
523
|
name: string;
|
|
508
524
|
email: string;
|
|
509
|
-
image: string | null;
|
|
510
525
|
role: "owner";
|
|
511
526
|
joinedAt: Date;
|
|
512
527
|
})[];
|
|
@@ -1724,7 +1739,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
1724
1739
|
user: {
|
|
1725
1740
|
name: string | null;
|
|
1726
1741
|
id: string;
|
|
1727
|
-
image: string | null;
|
|
1728
1742
|
} | null;
|
|
1729
1743
|
} & {
|
|
1730
1744
|
id: string;
|
|
@@ -1763,7 +1777,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
1763
1777
|
user: {
|
|
1764
1778
|
name: string | null;
|
|
1765
1779
|
id: string;
|
|
1766
|
-
image: string | null;
|
|
1767
1780
|
} | null;
|
|
1768
1781
|
} & {
|
|
1769
1782
|
id: string;
|
|
@@ -1791,7 +1804,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
1791
1804
|
user: {
|
|
1792
1805
|
name: string | null;
|
|
1793
1806
|
id: string;
|
|
1794
|
-
image: string | null;
|
|
1795
1807
|
} | null;
|
|
1796
1808
|
} & {
|
|
1797
1809
|
id: string;
|
|
@@ -1818,7 +1830,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
1818
1830
|
user: {
|
|
1819
1831
|
name: string | null;
|
|
1820
1832
|
id: string;
|
|
1821
|
-
image: string | null;
|
|
1822
1833
|
} | null;
|
|
1823
1834
|
} & {
|
|
1824
1835
|
id: string;
|
|
@@ -1839,7 +1850,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
1839
1850
|
user: {
|
|
1840
1851
|
name: string | null;
|
|
1841
1852
|
id: string;
|
|
1842
|
-
image: string | null;
|
|
1843
1853
|
} | null;
|
|
1844
1854
|
} & {
|
|
1845
1855
|
id: string;
|
package/dist/routers/auth.d.ts
CHANGED
|
@@ -20,6 +20,25 @@ export declare const auth: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
20
20
|
};
|
|
21
21
|
transformer: true;
|
|
22
22
|
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
23
|
+
updateProfile: import("@trpc/server").TRPCMutationProcedure<{
|
|
24
|
+
input: {
|
|
25
|
+
name: string;
|
|
26
|
+
};
|
|
27
|
+
output: {
|
|
28
|
+
success: boolean;
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
meta: object;
|
|
32
|
+
}>;
|
|
33
|
+
uploadProfilePicture: import("@trpc/server").TRPCMutationProcedure<{
|
|
34
|
+
input: void;
|
|
35
|
+
output: {
|
|
36
|
+
success: boolean;
|
|
37
|
+
message: string;
|
|
38
|
+
signedUrl: string;
|
|
39
|
+
};
|
|
40
|
+
meta: object;
|
|
41
|
+
}>;
|
|
23
42
|
signup: import("@trpc/server").TRPCMutationProcedure<{
|
|
24
43
|
input: {
|
|
25
44
|
name: string;
|
|
@@ -42,7 +61,6 @@ export declare const auth: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
42
61
|
id: string;
|
|
43
62
|
email: string | null;
|
|
44
63
|
name: string | null;
|
|
45
|
-
image: string | null;
|
|
46
64
|
token: string;
|
|
47
65
|
};
|
|
48
66
|
meta: object;
|
|
@@ -54,7 +72,6 @@ export declare const auth: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
54
72
|
id: string;
|
|
55
73
|
email: string | null;
|
|
56
74
|
name: string | null;
|
|
57
|
-
image: string | null;
|
|
58
75
|
};
|
|
59
76
|
};
|
|
60
77
|
meta: object;
|
package/dist/routers/auth.js
CHANGED
|
@@ -3,6 +3,8 @@ import { router, publicProcedure } from '../trpc.js';
|
|
|
3
3
|
import bcrypt from 'bcryptjs';
|
|
4
4
|
import { serialize } from 'cookie';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
|
+
import { TRPCError } from '@trpc/server';
|
|
7
|
+
import { supabaseClient } from 'src/lib/storage.js';
|
|
6
8
|
// Helper to create custom auth token
|
|
7
9
|
function createCustomAuthToken(userId) {
|
|
8
10
|
const secret = process.env.AUTH_SECRET;
|
|
@@ -16,6 +18,50 @@ function createCustomAuthToken(userId) {
|
|
|
16
18
|
return `${base64UserId}.${signature}`;
|
|
17
19
|
}
|
|
18
20
|
export const auth = router({
|
|
21
|
+
updateProfile: publicProcedure
|
|
22
|
+
.input(z.object({
|
|
23
|
+
name: z.string().min(1),
|
|
24
|
+
}))
|
|
25
|
+
.mutation(async ({ ctx, input }) => {
|
|
26
|
+
const { name } = input;
|
|
27
|
+
await ctx.db.user.update({
|
|
28
|
+
where: {
|
|
29
|
+
id: ctx.session.user.id,
|
|
30
|
+
},
|
|
31
|
+
data: {
|
|
32
|
+
name: name,
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
message: 'Profile updated successfully',
|
|
38
|
+
};
|
|
39
|
+
}),
|
|
40
|
+
uploadProfilePicture: publicProcedure
|
|
41
|
+
.mutation(async ({ ctx, input }) => {
|
|
42
|
+
const objectKey = `profile_picture_${ctx.session.user.id}`;
|
|
43
|
+
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
44
|
+
.from('media')
|
|
45
|
+
.createSignedUploadUrl(objectKey); // 5 minutes
|
|
46
|
+
if (signedUrlError) {
|
|
47
|
+
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
|
|
48
|
+
}
|
|
49
|
+
await ctx.db.fileAsset.create({
|
|
50
|
+
data: {
|
|
51
|
+
userId: ctx.session.user.id,
|
|
52
|
+
name: 'Profile Picture',
|
|
53
|
+
mimeType: 'image/jpeg',
|
|
54
|
+
size: 0,
|
|
55
|
+
bucket: 'media',
|
|
56
|
+
objectKey: objectKey,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
message: 'Profile picture uploaded successfully',
|
|
62
|
+
signedUrl: signedUrlData.signedUrl,
|
|
63
|
+
};
|
|
64
|
+
}),
|
|
19
65
|
signup: publicProcedure
|
|
20
66
|
.input(z.object({
|
|
21
67
|
name: z.string().min(1),
|
|
@@ -72,7 +118,6 @@ export const auth = router({
|
|
|
72
118
|
id: user.id,
|
|
73
119
|
email: user.email,
|
|
74
120
|
name: user.name,
|
|
75
|
-
image: user.image,
|
|
76
121
|
token: authToken
|
|
77
122
|
};
|
|
78
123
|
}),
|
|
@@ -92,7 +137,6 @@ export const auth = router({
|
|
|
92
137
|
id: user.id,
|
|
93
138
|
email: user.email,
|
|
94
139
|
name: user.name,
|
|
95
|
-
image: user.image
|
|
96
140
|
}
|
|
97
141
|
};
|
|
98
142
|
}),
|
package/dist/routers/chat.d.ts
CHANGED
|
@@ -42,7 +42,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
42
42
|
user: {
|
|
43
43
|
name: string | null;
|
|
44
44
|
id: string;
|
|
45
|
-
image: string | null;
|
|
46
45
|
} | null;
|
|
47
46
|
} & {
|
|
48
47
|
id: string;
|
|
@@ -81,7 +80,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
81
80
|
user: {
|
|
82
81
|
name: string | null;
|
|
83
82
|
id: string;
|
|
84
|
-
image: string | null;
|
|
85
83
|
} | null;
|
|
86
84
|
} & {
|
|
87
85
|
id: string;
|
|
@@ -109,7 +107,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
109
107
|
user: {
|
|
110
108
|
name: string | null;
|
|
111
109
|
id: string;
|
|
112
|
-
image: string | null;
|
|
113
110
|
} | null;
|
|
114
111
|
} & {
|
|
115
112
|
id: string;
|
|
@@ -136,7 +133,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
136
133
|
user: {
|
|
137
134
|
name: string | null;
|
|
138
135
|
id: string;
|
|
139
|
-
image: string | null;
|
|
140
136
|
} | null;
|
|
141
137
|
} & {
|
|
142
138
|
id: string;
|
|
@@ -157,7 +153,6 @@ export declare const chat: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
157
153
|
user: {
|
|
158
154
|
name: string | null;
|
|
159
155
|
id: string;
|
|
160
|
-
image: string | null;
|
|
161
156
|
} | null;
|
|
162
157
|
} & {
|
|
163
158
|
id: string;
|
package/dist/routers/chat.js
CHANGED
|
@@ -14,7 +14,13 @@ export const chat = router({
|
|
|
14
14
|
select: {
|
|
15
15
|
id: true,
|
|
16
16
|
name: true,
|
|
17
|
-
|
|
17
|
+
profilePicture: {
|
|
18
|
+
select: {
|
|
19
|
+
id: true,
|
|
20
|
+
name: true,
|
|
21
|
+
url: true,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
26
|
}
|
|
@@ -40,7 +46,6 @@ export const chat = router({
|
|
|
40
46
|
select: {
|
|
41
47
|
id: true,
|
|
42
48
|
name: true,
|
|
43
|
-
image: true,
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
}
|
|
@@ -62,7 +67,6 @@ export const chat = router({
|
|
|
62
67
|
select: {
|
|
63
68
|
id: true,
|
|
64
69
|
name: true,
|
|
65
|
-
image: true,
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
}
|
|
@@ -96,7 +100,6 @@ export const chat = router({
|
|
|
96
100
|
select: {
|
|
97
101
|
id: true,
|
|
98
102
|
name: true,
|
|
99
|
-
image: true,
|
|
100
103
|
}
|
|
101
104
|
},
|
|
102
105
|
}
|
|
@@ -122,7 +125,6 @@ export const chat = router({
|
|
|
122
125
|
select: {
|
|
123
126
|
id: true,
|
|
124
127
|
name: true,
|
|
125
|
-
image: true,
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
}
|
|
@@ -150,7 +152,6 @@ export const chat = router({
|
|
|
150
152
|
select: {
|
|
151
153
|
id: true,
|
|
152
154
|
name: true,
|
|
153
|
-
image: true,
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
}
|
|
@@ -171,7 +172,6 @@ export const chat = router({
|
|
|
171
172
|
select: {
|
|
172
173
|
id: true,
|
|
173
174
|
name: true,
|
|
174
|
-
image: true,
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
}
|
|
@@ -190,7 +190,6 @@ export const chat = router({
|
|
|
190
190
|
select: {
|
|
191
191
|
id: true,
|
|
192
192
|
name: true,
|
|
193
|
-
image: true,
|
|
194
193
|
}
|
|
195
194
|
}
|
|
196
195
|
}
|
|
@@ -209,7 +208,6 @@ export const chat = router({
|
|
|
209
208
|
select: {
|
|
210
209
|
id: true,
|
|
211
210
|
name: true,
|
|
212
|
-
image: true,
|
|
213
211
|
}
|
|
214
212
|
}
|
|
215
213
|
}
|
|
@@ -228,7 +226,6 @@ export const chat = router({
|
|
|
228
226
|
select: {
|
|
229
227
|
id: true,
|
|
230
228
|
name: true,
|
|
231
|
-
image: true,
|
|
232
229
|
}
|
|
233
230
|
},
|
|
234
231
|
}
|
|
@@ -42,14 +42,12 @@ export declare const members: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
42
42
|
id: string;
|
|
43
43
|
name: string;
|
|
44
44
|
email: string;
|
|
45
|
-
image: string | null;
|
|
46
45
|
role: "admin" | "member";
|
|
47
46
|
joinedAt: Date;
|
|
48
47
|
} | {
|
|
49
48
|
id: string;
|
|
50
49
|
name: string;
|
|
51
50
|
email: string;
|
|
52
|
-
image: string | null;
|
|
53
51
|
role: "owner";
|
|
54
52
|
joinedAt: Date;
|
|
55
53
|
})[];
|
package/dist/routers/members.js
CHANGED
|
@@ -37,7 +37,13 @@ export const members = router({
|
|
|
37
37
|
id: true,
|
|
38
38
|
name: true,
|
|
39
39
|
email: true,
|
|
40
|
-
|
|
40
|
+
profilePicture: {
|
|
41
|
+
select: {
|
|
42
|
+
id: true,
|
|
43
|
+
name: true,
|
|
44
|
+
url: true,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
41
47
|
}
|
|
42
48
|
},
|
|
43
49
|
members: {
|
|
@@ -47,7 +53,13 @@ export const members = router({
|
|
|
47
53
|
id: true,
|
|
48
54
|
name: true,
|
|
49
55
|
email: true,
|
|
50
|
-
|
|
56
|
+
profilePicture: {
|
|
57
|
+
select: {
|
|
58
|
+
id: true,
|
|
59
|
+
name: true,
|
|
60
|
+
url: true,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
}
|
|
@@ -66,7 +78,6 @@ export const members = router({
|
|
|
66
78
|
id: workspace.owner.id,
|
|
67
79
|
name: workspace.owner.name || 'Unknown',
|
|
68
80
|
email: workspace.owner.email || '',
|
|
69
|
-
image: workspace.owner.image,
|
|
70
81
|
role: 'owner',
|
|
71
82
|
joinedAt: workspace.createdAt,
|
|
72
83
|
},
|
|
@@ -74,7 +85,6 @@ export const members = router({
|
|
|
74
85
|
id: membership.user.id,
|
|
75
86
|
name: membership.user.name || 'Unknown',
|
|
76
87
|
email: membership.user.email || '',
|
|
77
|
-
image: membership.user.image,
|
|
78
88
|
role: membership.role,
|
|
79
89
|
joinedAt: membership.joinedAt,
|
|
80
90
|
}))
|
|
@@ -142,8 +142,8 @@ export declare const workspace: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
142
142
|
name: string;
|
|
143
143
|
id: string;
|
|
144
144
|
createdAt: Date;
|
|
145
|
-
userId: string;
|
|
146
|
-
workspaceId: string;
|
|
145
|
+
userId: string | null;
|
|
146
|
+
workspaceId: string | null;
|
|
147
147
|
mimeType: string;
|
|
148
148
|
size: number;
|
|
149
149
|
bucket: string | null;
|
|
@@ -408,14 +408,12 @@ export declare const workspace: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
408
408
|
id: string;
|
|
409
409
|
name: string;
|
|
410
410
|
email: string;
|
|
411
|
-
image: string | null;
|
|
412
411
|
role: "admin" | "member";
|
|
413
412
|
joinedAt: Date;
|
|
414
413
|
} | {
|
|
415
414
|
id: string;
|
|
416
415
|
name: string;
|
|
417
416
|
email: string;
|
|
418
|
-
image: string | null;
|
|
419
417
|
role: "owner";
|
|
420
418
|
joinedAt: Date;
|
|
421
419
|
})[];
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as trpcExpress from '@trpc/server/adapters/express';
|
|
|
8
8
|
import { appRouter } from './routers/_app.js';
|
|
9
9
|
import { createContext } from './context.js';
|
|
10
10
|
import { logger } from './lib/logger.js';
|
|
11
|
+
import { supabaseClient } from './lib/storage.js';
|
|
11
12
|
const PORT = process.env.PORT ? Number(process.env.PORT) : 3001;
|
|
12
13
|
async function main() {
|
|
13
14
|
const app = express();
|
|
@@ -35,6 +36,17 @@ async function main() {
|
|
|
35
36
|
app.get('/', (_req, res) => {
|
|
36
37
|
res.json({ ok: true, service: 'trpc-express', ts: Date.now() });
|
|
37
38
|
});
|
|
39
|
+
app.get('/profile-picture/:objectKey', async (req, res) => {
|
|
40
|
+
const { objectKey } = req.params;
|
|
41
|
+
const signedUrl = await supabaseClient.storage
|
|
42
|
+
.from('media')
|
|
43
|
+
.createSignedUrl(objectKey, 60 * 60 * 24 * 30);
|
|
44
|
+
if (signedUrl.error) {
|
|
45
|
+
return res.status(500).json({ error: 'Failed to generate signed URL' });
|
|
46
|
+
}
|
|
47
|
+
// res.json({ url: signedUrl.data.signedUrl });
|
|
48
|
+
res.redirect(signedUrl.data.signedUrl);
|
|
49
|
+
});
|
|
38
50
|
// tRPC mounted under /trpc
|
|
39
51
|
app.use('/trpc', trpcExpress.createExpressMiddleware({
|
|
40
52
|
router: appRouter,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goscribe/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"pusher-js": "^8.4.0",
|
|
43
43
|
"socket.io": "^4.8.1",
|
|
44
44
|
"superjson": "^2.2.2",
|
|
45
|
+
"uuid": "^13.0.0",
|
|
45
46
|
"zod": "^4.1.1"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
package/prisma/schema.prisma
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
|
|
2
1
|
generator client {
|
|
3
2
|
provider = "prisma-client-js"
|
|
4
3
|
}
|
|
5
4
|
|
|
6
5
|
datasource db {
|
|
7
|
-
provider
|
|
8
|
-
url
|
|
6
|
+
provider = "postgresql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
9
8
|
directUrl = env("DIRECT_URL") // for shadow db in migrations
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -39,49 +38,51 @@ enum QuestionType {
|
|
|
39
38
|
// NextAuth-compatible auth models (minimal)
|
|
40
39
|
//
|
|
41
40
|
model User {
|
|
42
|
-
id
|
|
43
|
-
name
|
|
44
|
-
email
|
|
45
|
-
emailVerified
|
|
46
|
-
passwordHash
|
|
47
|
-
|
|
48
|
-
session
|
|
41
|
+
id String @id @default(cuid())
|
|
42
|
+
name String?
|
|
43
|
+
email String? @unique
|
|
44
|
+
emailVerified DateTime?
|
|
45
|
+
passwordHash String? // for credentials login
|
|
46
|
+
profilePicture FileAsset? @relation(fields: [fileAssetId], references: [id])
|
|
47
|
+
session Session[]
|
|
49
48
|
|
|
50
49
|
// Ownership
|
|
51
|
-
folders
|
|
52
|
-
workspaces
|
|
53
|
-
invitedInWorkspaces
|
|
50
|
+
folders Folder[] @relation("UserFolders")
|
|
51
|
+
workspaces Workspace[] @relation("UserWorkspaces")
|
|
52
|
+
invitedInWorkspaces Workspace[] @relation("WorkspaceSharedWith") // many-to-many (deprecated)
|
|
54
53
|
workspaceMemberships WorkspaceMember[] // proper member management
|
|
55
|
-
uploads
|
|
56
|
-
artifacts
|
|
57
|
-
versions
|
|
58
|
-
|
|
54
|
+
uploads FileAsset[] @relation("UserUploads")
|
|
55
|
+
artifacts Artifact[] @relation("UserArtifacts")
|
|
56
|
+
versions ArtifactVersion[] @relation("UserArtifactVersions")
|
|
57
|
+
|
|
59
58
|
// Progress tracking
|
|
60
|
-
flashcardProgress FlashcardProgress[]
|
|
59
|
+
flashcardProgress FlashcardProgress[] @relation("UserFlashcardProgress")
|
|
61
60
|
worksheetQuestionProgress WorksheetQuestionProgress[]
|
|
62
|
-
|
|
61
|
+
|
|
63
62
|
// Invitations
|
|
64
63
|
sentInvitations WorkspaceInvitation[] @relation("UserInvitations")
|
|
65
64
|
|
|
66
65
|
notifications Notification[]
|
|
67
66
|
chats Chat[]
|
|
68
|
-
createdAt DateTime
|
|
69
|
-
updatedAt DateTime
|
|
67
|
+
createdAt DateTime @default(now())
|
|
68
|
+
updatedAt DateTime @updatedAt
|
|
69
|
+
fileAssetId String?
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
model Notification {
|
|
73
|
-
id
|
|
74
|
-
userId
|
|
75
|
-
user
|
|
76
|
-
content
|
|
77
|
-
read
|
|
73
|
+
id String @id @default(cuid())
|
|
74
|
+
userId String
|
|
75
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
76
|
+
content String
|
|
77
|
+
read Boolean @default(false)
|
|
78
78
|
createdAt DateTime @default(now())
|
|
79
79
|
updatedAt DateTime @updatedAt
|
|
80
80
|
}
|
|
81
|
+
|
|
81
82
|
model Session {
|
|
82
|
-
id
|
|
83
|
-
userId
|
|
84
|
-
user
|
|
83
|
+
id String @id @default(cuid())
|
|
84
|
+
userId String
|
|
85
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
85
86
|
expires DateTime
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -97,62 +98,62 @@ model VerificationToken {
|
|
|
97
98
|
// Filesystem-like structure
|
|
98
99
|
//
|
|
99
100
|
model Folder {
|
|
100
|
-
id
|
|
101
|
-
name
|
|
102
|
-
ownerId
|
|
103
|
-
owner
|
|
101
|
+
id String @id @default(cuid())
|
|
102
|
+
name String
|
|
103
|
+
ownerId String
|
|
104
|
+
owner User @relation("UserFolders", fields: [ownerId], references: [id], onDelete: Cascade)
|
|
104
105
|
|
|
105
106
|
// Nested folders
|
|
106
|
-
parentId
|
|
107
|
-
parent
|
|
108
|
-
children
|
|
109
|
-
color
|
|
107
|
+
parentId String?
|
|
108
|
+
parent Folder? @relation("FolderChildren", fields: [parentId], references: [id], onDelete: Cascade)
|
|
109
|
+
children Folder[] @relation("FolderChildren")
|
|
110
|
+
color String @default("#9D00FF")
|
|
110
111
|
|
|
111
112
|
// Files (workspaces) inside folders
|
|
112
113
|
workspaces Workspace[]
|
|
113
114
|
|
|
114
115
|
// Metadata
|
|
115
|
-
createdAt DateTime
|
|
116
|
-
updatedAt DateTime
|
|
116
|
+
createdAt DateTime @default(now())
|
|
117
|
+
updatedAt DateTime @updatedAt
|
|
117
118
|
|
|
118
119
|
// Helpful composite index: folders per owner + parent
|
|
119
120
|
@@index([ownerId, parentId])
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
model Workspace {
|
|
123
|
-
id String
|
|
124
|
+
id String @id @default(cuid())
|
|
124
125
|
title String
|
|
125
|
-
description String?
|
|
126
|
+
description String? // optional notes/description for the "file"
|
|
126
127
|
ownerId String
|
|
127
|
-
owner User
|
|
128
|
-
icon String
|
|
129
|
-
color String
|
|
128
|
+
owner User @relation("UserWorkspaces", fields: [ownerId], references: [id], onDelete: Cascade)
|
|
129
|
+
icon String @default("📄")
|
|
130
|
+
color String @default("#9D00FF")
|
|
130
131
|
|
|
131
132
|
// A workspace (file) lives in a folder (nullable = root)
|
|
132
|
-
folderId
|
|
133
|
-
folder
|
|
134
|
-
|
|
135
|
-
channels
|
|
133
|
+
folderId String?
|
|
134
|
+
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
|
135
|
+
|
|
136
|
+
channels Channel[]
|
|
136
137
|
|
|
137
|
-
sharedWith
|
|
138
|
-
members
|
|
139
|
-
fileBeingAnalyzed Boolean
|
|
138
|
+
sharedWith User[] @relation("WorkspaceSharedWith") // many-to-many for sharing (deprecated)
|
|
139
|
+
members WorkspaceMember[] // proper member management with roles
|
|
140
|
+
fileBeingAnalyzed Boolean @default(false)
|
|
140
141
|
|
|
141
142
|
analysisProgress Json?
|
|
142
143
|
|
|
143
144
|
needsAnalysis Boolean @default(false)
|
|
144
145
|
|
|
145
146
|
// Raw uploads attached to this workspace
|
|
146
|
-
uploads
|
|
147
|
+
uploads FileAsset[]
|
|
147
148
|
|
|
148
149
|
// AI outputs for this workspace (study guides, flashcards, etc.)
|
|
149
|
-
artifacts
|
|
150
|
-
|
|
150
|
+
artifacts Artifact[]
|
|
151
|
+
|
|
151
152
|
// Invitations
|
|
152
153
|
invitations WorkspaceInvitation[]
|
|
153
154
|
|
|
154
|
-
createdAt
|
|
155
|
-
updatedAt
|
|
155
|
+
createdAt DateTime @default(now())
|
|
156
|
+
updatedAt DateTime @updatedAt
|
|
156
157
|
|
|
157
158
|
@@index([ownerId, folderId])
|
|
158
159
|
}
|
|
@@ -162,25 +163,25 @@ model Channel {
|
|
|
162
163
|
workspaceId String
|
|
163
164
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
164
165
|
|
|
165
|
-
name
|
|
166
|
+
name String
|
|
166
167
|
|
|
167
|
-
createdAt
|
|
168
|
-
chats
|
|
168
|
+
createdAt DateTime @default(now())
|
|
169
|
+
chats Chat[]
|
|
169
170
|
|
|
170
171
|
@@index([workspaceId])
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
model Chat {
|
|
174
|
-
id
|
|
175
|
-
channelId
|
|
176
|
-
channel
|
|
175
|
+
id String @id @default(cuid())
|
|
176
|
+
channelId String
|
|
177
|
+
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
|
177
178
|
|
|
178
|
-
user
|
|
179
|
-
userId
|
|
180
|
-
message
|
|
179
|
+
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
180
|
+
userId String?
|
|
181
|
+
message String // chat message content
|
|
181
182
|
|
|
182
|
-
updatedAt
|
|
183
|
-
createdAt
|
|
183
|
+
updatedAt DateTime @updatedAt
|
|
184
|
+
createdAt DateTime @default(now())
|
|
184
185
|
|
|
185
186
|
@@index([channelId, createdAt])
|
|
186
187
|
}
|
|
@@ -189,25 +190,26 @@ model Chat {
|
|
|
189
190
|
// User uploads (source materials for AI)
|
|
190
191
|
//
|
|
191
192
|
model FileAsset {
|
|
192
|
-
id
|
|
193
|
-
workspaceId String
|
|
194
|
-
workspace
|
|
193
|
+
id String @id @default(cuid())
|
|
194
|
+
workspaceId String?
|
|
195
|
+
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
195
196
|
|
|
196
|
-
userId
|
|
197
|
-
user
|
|
197
|
+
userId String?
|
|
198
|
+
user User? @relation("UserUploads", fields: [userId], references: [id], onDelete: Cascade)
|
|
198
199
|
|
|
199
|
-
name
|
|
200
|
-
mimeType
|
|
201
|
-
size
|
|
202
|
-
bucket
|
|
203
|
-
objectKey
|
|
204
|
-
url
|
|
205
|
-
checksum
|
|
206
|
-
aiTranscription Json?
|
|
200
|
+
name String
|
|
201
|
+
mimeType String
|
|
202
|
+
size Int
|
|
203
|
+
bucket String?
|
|
204
|
+
objectKey String?
|
|
205
|
+
url String? // optional if serving via signed GET per-view
|
|
206
|
+
checksum String? // optional server-side integrity
|
|
207
|
+
aiTranscription Json? @default("{}")
|
|
207
208
|
|
|
208
|
-
meta
|
|
209
|
+
meta Json? // arbitrary metadata
|
|
209
210
|
|
|
210
211
|
createdAt DateTime @default(now())
|
|
212
|
+
User User[]
|
|
211
213
|
|
|
212
214
|
@@index([workspaceId])
|
|
213
215
|
@@index([userId, createdAt])
|
|
@@ -219,56 +221,56 @@ model FileAsset {
|
|
|
219
221
|
// - Some artifact types (flashcards, worksheet) have child rows
|
|
220
222
|
//
|
|
221
223
|
model Artifact {
|
|
222
|
-
id String
|
|
224
|
+
id String @id @default(cuid())
|
|
223
225
|
workspaceId String
|
|
224
|
-
workspace Workspace
|
|
226
|
+
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
225
227
|
|
|
226
|
-
type
|
|
227
|
-
title
|
|
228
|
-
isArchived
|
|
228
|
+
type ArtifactType
|
|
229
|
+
title String
|
|
230
|
+
isArchived Boolean @default(false)
|
|
229
231
|
|
|
230
|
-
generating
|
|
231
|
-
generatingMetadata Json?
|
|
232
|
+
generating Boolean @default(false)
|
|
233
|
+
generatingMetadata Json?
|
|
232
234
|
|
|
233
235
|
// Worksheet-specific fields
|
|
234
|
-
difficulty
|
|
235
|
-
estimatedTime String?
|
|
236
|
+
difficulty Difficulty? // only meaningful for WORKSHEET
|
|
237
|
+
estimatedTime String? // only meaningful for WORKSHEET
|
|
236
238
|
|
|
237
239
|
imageObjectKey String?
|
|
238
|
-
description
|
|
240
|
+
description String?
|
|
239
241
|
|
|
240
242
|
createdById String?
|
|
241
|
-
createdBy User?
|
|
243
|
+
createdBy User? @relation("UserArtifacts", fields: [createdById], references: [id], onDelete: SetNull)
|
|
242
244
|
|
|
243
|
-
versions
|
|
244
|
-
flashcards
|
|
245
|
-
questions
|
|
245
|
+
versions ArtifactVersion[] // text/transcript versions etc.
|
|
246
|
+
flashcards Flashcard[] // only meaningful for FLASHCARD_SET
|
|
247
|
+
questions WorksheetQuestion[] // only meaningful for WORKSHEET
|
|
246
248
|
podcastSegments PodcastSegment[] // only meaningful for PODCAST_EPISODE
|
|
247
249
|
|
|
248
|
-
createdAt
|
|
249
|
-
updatedAt
|
|
250
|
+
createdAt DateTime @default(now())
|
|
251
|
+
updatedAt DateTime @updatedAt
|
|
250
252
|
|
|
251
253
|
@@index([workspaceId, type])
|
|
252
254
|
}
|
|
253
255
|
|
|
254
256
|
model ArtifactVersion {
|
|
255
|
-
id
|
|
256
|
-
artifactId
|
|
257
|
-
artifact
|
|
257
|
+
id String @id @default(cuid())
|
|
258
|
+
artifactId String
|
|
259
|
+
artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
|
|
258
260
|
|
|
259
261
|
// Plain text content (e.g., Study Guide body, Meeting Summary text, Podcast transcript)
|
|
260
|
-
content
|
|
262
|
+
content String // rich text serialized as markdown/HTML stored as TEXT
|
|
261
263
|
|
|
262
264
|
// For Podcast episodes or other media, store URLs / durations / etc. in data
|
|
263
|
-
data
|
|
265
|
+
data Json? // e.g., { "audioUrl": "...", "durationSec": 312, "voice": "..." }
|
|
264
266
|
|
|
265
267
|
// Version sequencing (auto-increment per artifact)
|
|
266
|
-
version
|
|
268
|
+
version Int
|
|
267
269
|
|
|
268
270
|
createdById String?
|
|
269
|
-
createdBy User?
|
|
271
|
+
createdBy User? @relation("UserArtifactVersions", fields: [createdById], references: [id], onDelete: SetNull)
|
|
270
272
|
|
|
271
|
-
createdAt
|
|
273
|
+
createdAt DateTime @default(now())
|
|
272
274
|
|
|
273
275
|
@@unique([artifactId, version]) // each artifact has 1,2,3...
|
|
274
276
|
@@index([artifactId])
|
|
@@ -282,15 +284,15 @@ model Flashcard {
|
|
|
282
284
|
artifactId String
|
|
283
285
|
artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
|
|
284
286
|
|
|
285
|
-
front
|
|
286
|
-
back
|
|
287
|
-
tags
|
|
288
|
-
order
|
|
287
|
+
front String // question/term
|
|
288
|
+
back String // answer/definition
|
|
289
|
+
tags String[] // optional keywords
|
|
290
|
+
order Int @default(0)
|
|
289
291
|
|
|
290
292
|
// User progress tracking
|
|
291
|
-
progress
|
|
293
|
+
progress FlashcardProgress[]
|
|
292
294
|
|
|
293
|
-
createdAt
|
|
295
|
+
createdAt DateTime @default(now())
|
|
294
296
|
|
|
295
297
|
@@index([artifactId])
|
|
296
298
|
}
|
|
@@ -299,33 +301,33 @@ model Flashcard {
|
|
|
299
301
|
// User Progress on Flashcards (spaced repetition, mastery tracking)
|
|
300
302
|
//
|
|
301
303
|
model FlashcardProgress {
|
|
302
|
-
id
|
|
303
|
-
userId
|
|
304
|
-
user
|
|
305
|
-
|
|
304
|
+
id String @id @default(cuid())
|
|
305
|
+
userId String
|
|
306
|
+
user User @relation("UserFlashcardProgress", fields: [userId], references: [id], onDelete: Cascade)
|
|
307
|
+
|
|
306
308
|
flashcardId String
|
|
307
309
|
flashcard Flashcard @relation(fields: [flashcardId], references: [id], onDelete: Cascade)
|
|
308
310
|
|
|
309
311
|
// Study statistics
|
|
310
|
-
timesStudied Int
|
|
311
|
-
timesCorrect Int
|
|
312
|
-
timesIncorrect Int
|
|
313
|
-
timesIncorrectConsecutive Int
|
|
314
|
-
|
|
312
|
+
timesStudied Int @default(0)
|
|
313
|
+
timesCorrect Int @default(0)
|
|
314
|
+
timesIncorrect Int @default(0)
|
|
315
|
+
timesIncorrectConsecutive Int @default(0) // Track consecutive failures
|
|
316
|
+
|
|
315
317
|
// Spaced repetition data
|
|
316
|
-
easeFactor
|
|
317
|
-
interval
|
|
318
|
-
repetitions
|
|
319
|
-
|
|
318
|
+
easeFactor Float @default(2.5) // SM-2 algorithm ease factor
|
|
319
|
+
interval Int @default(0) // Days until next review
|
|
320
|
+
repetitions Int @default(0) // Consecutive correct answers
|
|
321
|
+
|
|
320
322
|
// Mastery level (0-100)
|
|
321
|
-
masteryLevel
|
|
322
|
-
|
|
323
|
+
masteryLevel Int @default(0)
|
|
324
|
+
|
|
323
325
|
// Timestamps
|
|
324
|
-
lastStudiedAt
|
|
325
|
-
nextReviewAt
|
|
326
|
-
|
|
327
|
-
createdAt
|
|
328
|
-
updatedAt
|
|
326
|
+
lastStudiedAt DateTime?
|
|
327
|
+
nextReviewAt DateTime?
|
|
328
|
+
|
|
329
|
+
createdAt DateTime @default(now())
|
|
330
|
+
updatedAt DateTime @updatedAt
|
|
329
331
|
|
|
330
332
|
@@unique([userId, flashcardId])
|
|
331
333
|
@@index([userId, nextReviewAt])
|
|
@@ -336,9 +338,9 @@ model FlashcardProgress {
|
|
|
336
338
|
// Worksheet Questions (child items of a WORKSHEET Artifact)
|
|
337
339
|
//
|
|
338
340
|
model WorksheetQuestion {
|
|
339
|
-
id String
|
|
341
|
+
id String @id @default(cuid())
|
|
340
342
|
artifactId String
|
|
341
|
-
artifact Artifact
|
|
343
|
+
artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
|
|
342
344
|
|
|
343
345
|
prompt String
|
|
344
346
|
answer String?
|
|
@@ -346,35 +348,35 @@ model WorksheetQuestion {
|
|
|
346
348
|
difficulty Difficulty @default(MEDIUM)
|
|
347
349
|
order Int @default(0)
|
|
348
350
|
|
|
349
|
-
meta
|
|
351
|
+
meta Json? // e.g., { "choices": ["A","B","C","D"], "correct": 1, "options": [...] }
|
|
350
352
|
|
|
351
|
-
createdAt
|
|
353
|
+
createdAt DateTime @default(now())
|
|
354
|
+
progress WorksheetQuestionProgress[]
|
|
352
355
|
|
|
353
356
|
@@index([artifactId])
|
|
354
|
-
progress WorksheetQuestionProgress[]
|
|
355
357
|
}
|
|
356
358
|
|
|
357
359
|
//
|
|
358
360
|
// Per-user progress for Worksheet Questions
|
|
359
361
|
//
|
|
360
362
|
model WorksheetQuestionProgress {
|
|
361
|
-
id
|
|
362
|
-
worksheetQuestionId
|
|
363
|
-
worksheetQuestion
|
|
363
|
+
id String @id @default(cuid())
|
|
364
|
+
worksheetQuestionId String
|
|
365
|
+
worksheetQuestion WorksheetQuestion @relation(fields: [worksheetQuestionId], references: [id], onDelete: Cascade)
|
|
364
366
|
|
|
365
|
-
userId
|
|
366
|
-
user
|
|
367
|
+
userId String
|
|
368
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
367
369
|
|
|
368
|
-
modified
|
|
369
|
-
userAnswer
|
|
370
|
-
correct
|
|
371
|
-
completedAt
|
|
372
|
-
attempts
|
|
373
|
-
timeSpentSec
|
|
374
|
-
meta
|
|
370
|
+
modified Boolean @default(false)
|
|
371
|
+
userAnswer String?
|
|
372
|
+
correct Boolean? @default(false)
|
|
373
|
+
completedAt DateTime?
|
|
374
|
+
attempts Int @default(0)
|
|
375
|
+
timeSpentSec Int?
|
|
376
|
+
meta Json?
|
|
375
377
|
|
|
376
|
-
createdAt
|
|
377
|
-
updatedAt
|
|
378
|
+
createdAt DateTime @default(now())
|
|
379
|
+
updatedAt DateTime @updatedAt
|
|
378
380
|
|
|
379
381
|
@@unique([worksheetQuestionId, userId])
|
|
380
382
|
@@index([userId])
|
|
@@ -387,15 +389,15 @@ model WorkspaceMember {
|
|
|
387
389
|
id String @id @default(cuid())
|
|
388
390
|
workspaceId String
|
|
389
391
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
390
|
-
|
|
391
|
-
userId
|
|
392
|
-
user
|
|
393
|
-
|
|
394
|
-
role
|
|
395
|
-
|
|
396
|
-
joinedAt
|
|
397
|
-
updatedAt
|
|
398
|
-
|
|
392
|
+
|
|
393
|
+
userId String
|
|
394
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
395
|
+
|
|
396
|
+
role String @default("member") // "owner", "admin", "member"
|
|
397
|
+
|
|
398
|
+
joinedAt DateTime @default(now())
|
|
399
|
+
updatedAt DateTime @updatedAt
|
|
400
|
+
|
|
399
401
|
@@unique([workspaceId, userId]) // One membership per user per workspace
|
|
400
402
|
@@index([workspaceId])
|
|
401
403
|
@@index([userId])
|
|
@@ -408,20 +410,20 @@ model WorkspaceInvitation {
|
|
|
408
410
|
id String @id @default(cuid())
|
|
409
411
|
workspaceId String
|
|
410
412
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
411
|
-
|
|
412
|
-
email
|
|
413
|
-
role
|
|
414
|
-
token
|
|
415
|
-
|
|
413
|
+
|
|
414
|
+
email String
|
|
415
|
+
role String @default("member") // "owner", "admin", "member"
|
|
416
|
+
token String @unique @default(cuid()) // UUID for invitation link
|
|
417
|
+
|
|
416
418
|
invitedById String
|
|
417
|
-
invitedBy User
|
|
418
|
-
|
|
419
|
-
acceptedAt
|
|
420
|
-
expiresAt
|
|
421
|
-
|
|
422
|
-
createdAt
|
|
423
|
-
updatedAt
|
|
424
|
-
|
|
419
|
+
invitedBy User @relation("UserInvitations", fields: [invitedById], references: [id], onDelete: Cascade)
|
|
420
|
+
|
|
421
|
+
acceptedAt DateTime?
|
|
422
|
+
expiresAt DateTime @default(dbgenerated("NOW() + INTERVAL '7 days'"))
|
|
423
|
+
|
|
424
|
+
createdAt DateTime @default(now())
|
|
425
|
+
updatedAt DateTime @updatedAt
|
|
426
|
+
|
|
425
427
|
@@unique([workspaceId, email]) // One invitation per email per workspace
|
|
426
428
|
@@index([token])
|
|
427
429
|
@@index([workspaceId])
|
|
@@ -435,26 +437,26 @@ model PodcastSegment {
|
|
|
435
437
|
artifactId String
|
|
436
438
|
artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
|
|
437
439
|
|
|
438
|
-
title
|
|
439
|
-
content
|
|
440
|
-
startTime
|
|
441
|
-
duration
|
|
442
|
-
order
|
|
443
|
-
|
|
440
|
+
title String
|
|
441
|
+
content String // Full text content of the segment
|
|
442
|
+
startTime Int // Start time in seconds
|
|
443
|
+
duration Int // Duration in seconds
|
|
444
|
+
order Int // Display order within the episode
|
|
445
|
+
|
|
444
446
|
// Audio file reference
|
|
445
|
-
objectKey
|
|
446
|
-
audioUrl
|
|
447
|
-
|
|
447
|
+
objectKey String? // Google Cloud Storage object key
|
|
448
|
+
audioUrl String? // Cached signed URL (temporary)
|
|
449
|
+
|
|
448
450
|
// Metadata
|
|
449
|
-
keyPoints
|
|
450
|
-
meta
|
|
451
|
-
|
|
452
|
-
generating
|
|
451
|
+
keyPoints String[] // Array of key points
|
|
452
|
+
meta Json? // Additional metadata (voice settings, etc.)
|
|
453
|
+
|
|
454
|
+
generating Boolean @default(false)
|
|
453
455
|
generatingMetadata Json? // Additional metadata (voice settings, etc.)
|
|
454
|
-
|
|
455
|
-
createdAt
|
|
456
|
-
updatedAt
|
|
456
|
+
|
|
457
|
+
createdAt DateTime @default(now())
|
|
458
|
+
updatedAt DateTime @updatedAt
|
|
457
459
|
|
|
458
460
|
@@index([artifactId, order]) // For efficient ordering
|
|
459
461
|
@@index([artifactId, startTime]) // For time-based queries
|
|
460
|
-
}
|
|
462
|
+
}
|
package/src/routers/auth.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { router, publicProcedure, authedProcedure } from '../trpc.js';
|
|
|
3
3
|
import bcrypt from 'bcryptjs';
|
|
4
4
|
import { serialize } from 'cookie';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
|
+
import { TRPCError } from '@trpc/server';
|
|
7
|
+
import { supabaseClient } from 'src/lib/storage.js';
|
|
6
8
|
|
|
7
9
|
// Helper to create custom auth token
|
|
8
10
|
function createCustomAuthToken(userId: string): string {
|
|
@@ -19,6 +21,54 @@ function createCustomAuthToken(userId: string): string {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export const auth = router({
|
|
24
|
+
updateProfile: publicProcedure
|
|
25
|
+
.input(z.object({
|
|
26
|
+
name: z.string().min(1),
|
|
27
|
+
}))
|
|
28
|
+
.mutation(async ({ctx, input}) => {
|
|
29
|
+
const { name } = input;
|
|
30
|
+
|
|
31
|
+
await ctx.db.user.update({
|
|
32
|
+
where: {
|
|
33
|
+
id: ctx.session.user.id,
|
|
34
|
+
},
|
|
35
|
+
data: {
|
|
36
|
+
name: name,
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
message: 'Profile updated successfully',
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
uploadProfilePicture: publicProcedure
|
|
46
|
+
.mutation(async ({ctx, input}) => {
|
|
47
|
+
const objectKey = `profile_picture_${ctx.session.user.id}`;
|
|
48
|
+
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
49
|
+
.from('media')
|
|
50
|
+
.createSignedUploadUrl(objectKey); // 5 minutes
|
|
51
|
+
if (signedUrlError) {
|
|
52
|
+
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `Failed to generate upload URL: ${signedUrlError.message}` });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await ctx.db.fileAsset.create({
|
|
56
|
+
data: {
|
|
57
|
+
userId: ctx.session.user.id,
|
|
58
|
+
name: 'Profile Picture',
|
|
59
|
+
mimeType: 'image/jpeg',
|
|
60
|
+
size: 0,
|
|
61
|
+
bucket: 'media',
|
|
62
|
+
objectKey: objectKey,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
message: 'Profile picture uploaded successfully',
|
|
69
|
+
signedUrl: signedUrlData.signedUrl,
|
|
70
|
+
};
|
|
71
|
+
}),
|
|
22
72
|
signup: publicProcedure
|
|
23
73
|
.input(z.object({
|
|
24
74
|
name: z.string().min(1),
|
|
@@ -85,7 +135,6 @@ export const auth = router({
|
|
|
85
135
|
id: user.id,
|
|
86
136
|
email: user.email,
|
|
87
137
|
name: user.name,
|
|
88
|
-
image: user.image,
|
|
89
138
|
token: authToken
|
|
90
139
|
};
|
|
91
140
|
}),
|
|
@@ -108,7 +157,6 @@ export const auth = router({
|
|
|
108
157
|
id: user.id,
|
|
109
158
|
email: user.email,
|
|
110
159
|
name: user.name,
|
|
111
|
-
image: user.image
|
|
112
160
|
}
|
|
113
161
|
};
|
|
114
162
|
}),
|
package/src/routers/chat.ts
CHANGED
|
@@ -15,7 +15,13 @@ export const chat = router({
|
|
|
15
15
|
select: {
|
|
16
16
|
id: true,
|
|
17
17
|
name: true,
|
|
18
|
-
|
|
18
|
+
profilePicture: {
|
|
19
|
+
select: {
|
|
20
|
+
id: true,
|
|
21
|
+
name: true,
|
|
22
|
+
url: true,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
}
|
|
@@ -41,7 +47,6 @@ export const chat = router({
|
|
|
41
47
|
select: {
|
|
42
48
|
id: true,
|
|
43
49
|
name: true,
|
|
44
|
-
image: true,
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
}
|
|
@@ -65,7 +70,6 @@ export const chat = router({
|
|
|
65
70
|
select: {
|
|
66
71
|
id: true,
|
|
67
72
|
name: true,
|
|
68
|
-
image: true,
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
}
|
|
@@ -101,7 +105,6 @@ export const chat = router({
|
|
|
101
105
|
select: {
|
|
102
106
|
id: true,
|
|
103
107
|
name: true,
|
|
104
|
-
image: true,
|
|
105
108
|
}
|
|
106
109
|
},
|
|
107
110
|
}
|
|
@@ -127,7 +130,6 @@ export const chat = router({
|
|
|
127
130
|
select: {
|
|
128
131
|
id: true,
|
|
129
132
|
name: true,
|
|
130
|
-
image: true,
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
135
|
}
|
|
@@ -155,7 +157,6 @@ export const chat = router({
|
|
|
155
157
|
select: {
|
|
156
158
|
id: true,
|
|
157
159
|
name: true,
|
|
158
|
-
image: true,
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -176,7 +177,6 @@ export const chat = router({
|
|
|
176
177
|
select: {
|
|
177
178
|
id: true,
|
|
178
179
|
name: true,
|
|
179
|
-
image: true,
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
}
|
|
@@ -195,7 +195,6 @@ export const chat = router({
|
|
|
195
195
|
select: {
|
|
196
196
|
id: true,
|
|
197
197
|
name: true,
|
|
198
|
-
image: true,
|
|
199
198
|
}
|
|
200
199
|
}
|
|
201
200
|
}
|
|
@@ -214,7 +213,6 @@ export const chat = router({
|
|
|
214
213
|
select: {
|
|
215
214
|
id: true,
|
|
216
215
|
name: true,
|
|
217
|
-
image: true,
|
|
218
216
|
}
|
|
219
217
|
}
|
|
220
218
|
}
|
|
@@ -233,7 +231,6 @@ export const chat = router({
|
|
|
233
231
|
select: {
|
|
234
232
|
id: true,
|
|
235
233
|
name: true,
|
|
236
|
-
image: true,
|
|
237
234
|
}
|
|
238
235
|
},
|
|
239
236
|
}
|
package/src/routers/members.ts
CHANGED
|
@@ -38,7 +38,13 @@ export const members = router({
|
|
|
38
38
|
id: true,
|
|
39
39
|
name: true,
|
|
40
40
|
email: true,
|
|
41
|
-
|
|
41
|
+
profilePicture: {
|
|
42
|
+
select: {
|
|
43
|
+
id: true,
|
|
44
|
+
name: true,
|
|
45
|
+
url: true,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
42
48
|
}
|
|
43
49
|
},
|
|
44
50
|
members: {
|
|
@@ -48,7 +54,13 @@ export const members = router({
|
|
|
48
54
|
id: true,
|
|
49
55
|
name: true,
|
|
50
56
|
email: true,
|
|
51
|
-
|
|
57
|
+
profilePicture: {
|
|
58
|
+
select: {
|
|
59
|
+
id: true,
|
|
60
|
+
name: true,
|
|
61
|
+
url: true,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
66
|
}
|
|
@@ -69,7 +81,6 @@ export const members = router({
|
|
|
69
81
|
id: workspace.owner.id,
|
|
70
82
|
name: workspace.owner.name || 'Unknown',
|
|
71
83
|
email: workspace.owner.email || '',
|
|
72
|
-
image: workspace.owner.image,
|
|
73
84
|
role: 'owner' as const,
|
|
74
85
|
joinedAt: workspace.createdAt,
|
|
75
86
|
},
|
|
@@ -77,7 +88,6 @@ export const members = router({
|
|
|
77
88
|
id: membership.user.id,
|
|
78
89
|
name: membership.user.name || 'Unknown',
|
|
79
90
|
email: membership.user.email || '',
|
|
80
|
-
image: membership.user.image,
|
|
81
91
|
role: membership.role as 'admin' | 'member',
|
|
82
92
|
joinedAt: membership.joinedAt,
|
|
83
93
|
}))
|
package/src/server.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { appRouter } from './routers/_app.js';
|
|
|
10
10
|
import { createContext } from './context.js';
|
|
11
11
|
import { prisma } from './lib/prisma.js';
|
|
12
12
|
import { logger } from './lib/logger.js';
|
|
13
|
+
import { supabaseClient } from './lib/storage.js';
|
|
13
14
|
|
|
14
15
|
const PORT = process.env.PORT ? Number(process.env.PORT) : 3001;
|
|
15
16
|
|
|
@@ -45,6 +46,18 @@ async function main() {
|
|
|
45
46
|
res.json({ ok: true, service: 'trpc-express', ts: Date.now() });
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
app.get('/profile-picture/:objectKey', async (req, res) => {
|
|
50
|
+
const { objectKey } = req.params;
|
|
51
|
+
const signedUrl = await supabaseClient.storage
|
|
52
|
+
.from('media')
|
|
53
|
+
.createSignedUrl(objectKey, 60 * 60 * 24 * 30);
|
|
54
|
+
if (signedUrl.error) {
|
|
55
|
+
return res.status(500).json({ error: 'Failed to generate signed URL' });
|
|
56
|
+
}
|
|
57
|
+
// res.json({ url: signedUrl.data.signedUrl });
|
|
58
|
+
res.redirect(signedUrl.data.signedUrl);
|
|
59
|
+
});
|
|
60
|
+
|
|
48
61
|
// tRPC mounted under /trpc
|
|
49
62
|
app.use(
|
|
50
63
|
'/trpc',
|