@amogads/ui 1.0.2 → 1.1.1
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/README.md +295 -287
- package/dist/index.d.ts +1 -952
- package/dist/index.js +4446 -1698
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4108 -1281
- package/dist/index.mjs.map +1 -1
- package/dist/pages.d.ts +16 -0
- package/dist/pages.js +40135 -0
- package/dist/pages.js.map +1 -0
- package/dist/pages.mjs +40482 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +520 -0
- package/dist/sdk.js.map +1 -0
- package/dist/sdk.mjs +476 -0
- package/dist/sdk.mjs.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +2713 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +2662 -0
- package/dist/server.mjs.map +1 -0
- package/dist/services.d.ts +1 -0
- package/dist/services.js +2042 -0
- package/dist/services.js.map +1 -0
- package/dist/services.mjs +1983 -0
- package/dist/services.mjs.map +1 -0
- package/dist/stores.d.ts +1 -0
- package/dist/stores.js +559 -0
- package/dist/stores.js.map +1 -0
- package/dist/stores.mjs +531 -0
- package/dist/stores.mjs.map +1 -0
- package/dist/tokens.d.ts +1 -56
- package/dist/tokens.js +0 -1
- package/dist/tokens.js.map +1 -1
- package/package.json +257 -232
- package/dist/index.d.mts +0 -952
- package/dist/tokens.d.mts +0 -56
|
@@ -0,0 +1,1983 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/lib/supabase/client.ts
|
|
4
|
+
import { createBrowserClient } from "@supabase/ssr";
|
|
5
|
+
var clientSingleton = null;
|
|
6
|
+
function createClient() {
|
|
7
|
+
if (typeof window === "undefined") {
|
|
8
|
+
return createBrowserClient(
|
|
9
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
10
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
if (!clientSingleton) {
|
|
14
|
+
clientSingleton = createBrowserClient(
|
|
15
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
16
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return clientSingleton;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/stores/auth-store.ts
|
|
23
|
+
import { create } from "zustand";
|
|
24
|
+
|
|
25
|
+
// src/lib/cookies.ts
|
|
26
|
+
var DEFAULT_MAX_AGE = 60 * 60 * 24 * 7;
|
|
27
|
+
function getCookie(name) {
|
|
28
|
+
if (typeof document === "undefined") return void 0;
|
|
29
|
+
const value = `; ${document.cookie}`;
|
|
30
|
+
const parts = value.split(`; ${name}=`);
|
|
31
|
+
if (parts.length === 2) {
|
|
32
|
+
const cookieValue = parts.pop()?.split(";").shift();
|
|
33
|
+
return cookieValue;
|
|
34
|
+
}
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
function setCookie(name, value, maxAge = DEFAULT_MAX_AGE) {
|
|
38
|
+
if (typeof document === "undefined") return;
|
|
39
|
+
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`;
|
|
40
|
+
}
|
|
41
|
+
function removeCookie(name) {
|
|
42
|
+
if (typeof document === "undefined") return;
|
|
43
|
+
document.cookie = `${name}=; path=/; max-age=0`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/stores/auth-store.ts
|
|
47
|
+
var ACCESS_TOKEN = "thisisjustarandomstring";
|
|
48
|
+
var USER_DATA = "auth_user_data";
|
|
49
|
+
var useAuthStore = create()((set) => {
|
|
50
|
+
const cookieState = getCookie(ACCESS_TOKEN);
|
|
51
|
+
let initToken = "";
|
|
52
|
+
if (cookieState) {
|
|
53
|
+
try {
|
|
54
|
+
initToken = JSON.parse(cookieState);
|
|
55
|
+
} catch {
|
|
56
|
+
removeCookie(ACCESS_TOKEN);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const userCookie = getCookie(USER_DATA);
|
|
60
|
+
let initUser = null;
|
|
61
|
+
if (userCookie) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(decodeURIComponent(userCookie));
|
|
64
|
+
if (parsed.exp && parsed.exp > Date.now()) {
|
|
65
|
+
initUser = parsed;
|
|
66
|
+
} else {
|
|
67
|
+
removeCookie(ACCESS_TOKEN);
|
|
68
|
+
removeCookie(USER_DATA);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
initUser = null;
|
|
72
|
+
removeCookie(USER_DATA);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
auth: {
|
|
77
|
+
user: initUser,
|
|
78
|
+
setUser: (user) => set((state) => {
|
|
79
|
+
if (user) {
|
|
80
|
+
setCookie(USER_DATA, encodeURIComponent(JSON.stringify(user)));
|
|
81
|
+
} else {
|
|
82
|
+
removeCookie(USER_DATA);
|
|
83
|
+
}
|
|
84
|
+
return { ...state, auth: { ...state.auth, user } };
|
|
85
|
+
}),
|
|
86
|
+
accessToken: initUser ? initToken : "",
|
|
87
|
+
setAccessToken: (accessToken) => set((state) => {
|
|
88
|
+
setCookie(ACCESS_TOKEN, JSON.stringify(accessToken));
|
|
89
|
+
return { ...state, auth: { ...state.auth, accessToken } };
|
|
90
|
+
}),
|
|
91
|
+
resetAccessToken: () => set((state) => {
|
|
92
|
+
removeCookie(ACCESS_TOKEN);
|
|
93
|
+
return { ...state, auth: { ...state.auth, accessToken: "" } };
|
|
94
|
+
}),
|
|
95
|
+
reset: () => set((state) => {
|
|
96
|
+
removeCookie(ACCESS_TOKEN);
|
|
97
|
+
removeCookie(USER_DATA);
|
|
98
|
+
return {
|
|
99
|
+
...state,
|
|
100
|
+
auth: { ...state.auth, user: null, accessToken: "" }
|
|
101
|
+
};
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// src/lib/auth.ts
|
|
108
|
+
import GoogleProvider from "next-auth/providers/google";
|
|
109
|
+
import { createClient as createSupabaseClient } from "@supabase/supabase-js";
|
|
110
|
+
import crypto2 from "crypto";
|
|
111
|
+
var SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
112
|
+
var SUPABASE_KEY = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || "";
|
|
113
|
+
function stringToUuid(str) {
|
|
114
|
+
if (!str) return crypto2.randomUUID();
|
|
115
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
116
|
+
if (uuidRegex.test(str)) {
|
|
117
|
+
return str;
|
|
118
|
+
}
|
|
119
|
+
const hash = crypto2.createHash("md5").update(str).digest("hex");
|
|
120
|
+
return `${hash.substring(0, 8)}-${hash.substring(8, 12)}-4${hash.substring(13, 16)}-a${hash.substring(17, 20)}-${hash.substring(20, 32)}`;
|
|
121
|
+
}
|
|
122
|
+
var authOptions = {
|
|
123
|
+
providers: [
|
|
124
|
+
GoogleProvider({
|
|
125
|
+
clientId: process.env.GOOGLE_CLIENT_ID || process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "",
|
|
126
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET || process.env.NEXT_PUBLIC_GOOGLE_SECRET_ID || "",
|
|
127
|
+
authorization: {
|
|
128
|
+
params: {
|
|
129
|
+
prompt: "select_account",
|
|
130
|
+
access_type: "offline",
|
|
131
|
+
response_type: "code"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
checks: ["none"]
|
|
135
|
+
})
|
|
136
|
+
],
|
|
137
|
+
secret: process.env.NEXTAUTH_SECRET || "secret_next_auth_shadcn_admin_key_2026_super_secure",
|
|
138
|
+
session: {
|
|
139
|
+
strategy: "jwt",
|
|
140
|
+
maxAge: 30 * 24 * 60 * 60
|
|
141
|
+
// 30 days
|
|
142
|
+
},
|
|
143
|
+
callbacks: {
|
|
144
|
+
async redirect({ url, baseUrl }) {
|
|
145
|
+
try {
|
|
146
|
+
const { cookies } = await import("next/headers");
|
|
147
|
+
const cookieStore = await cookies();
|
|
148
|
+
const isMobileAuth = cookieStore.get("mobile_auth")?.value === "true" || url.includes("is_mobile=true");
|
|
149
|
+
if (isMobileAuth) {
|
|
150
|
+
console.log("\u{1F4F1} [NextAuth Redirect Callback] Mobile auth detected. Redirecting to /auth/callback?is_mobile=true");
|
|
151
|
+
return `${baseUrl}/auth/callback?is_mobile=true&next=/`;
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error("\u274C [NextAuth Redirect Callback] Error inspecting cookies:", err);
|
|
155
|
+
}
|
|
156
|
+
if (url.includes("/auth/callback")) return url;
|
|
157
|
+
if (url.startsWith("/")) return `${baseUrl}${url}`;
|
|
158
|
+
else if (new URL(url).origin === baseUrl) return url;
|
|
159
|
+
return baseUrl;
|
|
160
|
+
},
|
|
161
|
+
async signIn({ user }) {
|
|
162
|
+
if (!user.email) return false;
|
|
163
|
+
try {
|
|
164
|
+
if (SUPABASE_URL && SUPABASE_KEY) {
|
|
165
|
+
const supabase2 = createSupabaseClient(SUPABASE_URL, SUPABASE_KEY);
|
|
166
|
+
const fallbackUuid = stringToUuid(user.id || user.email);
|
|
167
|
+
const { data: existingProfile } = await supabase2.from("profiles").select("id, auth_user_id").eq("email", user.email.toLowerCase()).maybeSingle();
|
|
168
|
+
const profileId = existingProfile?.id || fallbackUuid;
|
|
169
|
+
const profileData = {
|
|
170
|
+
id: profileId,
|
|
171
|
+
name: user.name || user.email.split("@")[0],
|
|
172
|
+
email: user.email.toLowerCase(),
|
|
173
|
+
avatar: user.image || null,
|
|
174
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
175
|
+
};
|
|
176
|
+
profileData.auth_user_id = existingProfile?.auth_user_id || profileId;
|
|
177
|
+
if (existingProfile) {
|
|
178
|
+
await supabase2.from("profiles").update(profileData).eq("id", existingProfile.id);
|
|
179
|
+
} else {
|
|
180
|
+
await supabase2.from("profiles").insert(profileData);
|
|
181
|
+
}
|
|
182
|
+
user.id = profileId;
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error("[NextAuth] Error syncing user to profiles table:", err);
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
async jwt({ token, user }) {
|
|
190
|
+
if (user) {
|
|
191
|
+
token.id = stringToUuid(user.id || user.email);
|
|
192
|
+
token.email = user.email;
|
|
193
|
+
token.name = user.name;
|
|
194
|
+
token.picture = user.image;
|
|
195
|
+
} else if (token.sub && !stringToUuid(token.sub)) {
|
|
196
|
+
token.id = stringToUuid(token.sub);
|
|
197
|
+
}
|
|
198
|
+
return token;
|
|
199
|
+
},
|
|
200
|
+
async session({ session, token }) {
|
|
201
|
+
if (session.user) {
|
|
202
|
+
const canonicalId = stringToUuid(token.id || token.sub || session.user.email);
|
|
203
|
+
session.user.id = canonicalId;
|
|
204
|
+
session.user.picture = token.picture || token.image;
|
|
205
|
+
}
|
|
206
|
+
return session;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
pages: {
|
|
210
|
+
signIn: "/sign-in",
|
|
211
|
+
error: "/sign-in"
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/features/chattemplate/shared/api/auth.ts
|
|
216
|
+
async function getAccessToken() {
|
|
217
|
+
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || "";
|
|
218
|
+
try {
|
|
219
|
+
const storeToken = useAuthStore.getState().auth.accessToken;
|
|
220
|
+
if (storeToken && typeof storeToken === "string" && storeToken.split(".").length === 3) {
|
|
221
|
+
return storeToken;
|
|
222
|
+
}
|
|
223
|
+
const supabase2 = createClient();
|
|
224
|
+
const { data: { session } } = await supabase2.auth.getSession();
|
|
225
|
+
if (session?.access_token && session.access_token.split(".").length === 3) {
|
|
226
|
+
return session.access_token;
|
|
227
|
+
}
|
|
228
|
+
return supabaseKey;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return supabaseKey;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/features/chattemplate/shared/api/headers.ts
|
|
235
|
+
async function getHeaders(customHeaders = {}) {
|
|
236
|
+
const token = await getAccessToken();
|
|
237
|
+
const apiKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || "";
|
|
238
|
+
const is3PartJwt = typeof token === "string" && token.split(".").length === 3;
|
|
239
|
+
const validBearerToken = is3PartJwt ? token : apiKey;
|
|
240
|
+
return {
|
|
241
|
+
"apikey": apiKey,
|
|
242
|
+
"Authorization": `Bearer ${validBearerToken}`,
|
|
243
|
+
"Content-Type": "application/json",
|
|
244
|
+
...customHeaders
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/features/chattemplate/shared/api/errorHandler.ts
|
|
249
|
+
var ApiError = class extends Error {
|
|
250
|
+
constructor(message, code, details, hint) {
|
|
251
|
+
super(message);
|
|
252
|
+
this.name = "ApiError";
|
|
253
|
+
this.code = code;
|
|
254
|
+
this.details = details;
|
|
255
|
+
this.hint = hint;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
async function handleError(response) {
|
|
259
|
+
let errorData;
|
|
260
|
+
try {
|
|
261
|
+
errorData = await response.json();
|
|
262
|
+
} catch {
|
|
263
|
+
throw new ApiError(response.statusText || "An unknown network error occurred");
|
|
264
|
+
}
|
|
265
|
+
const message = errorData?.message || errorData?.error_description || "API request failed";
|
|
266
|
+
const code = errorData?.code;
|
|
267
|
+
const details = errorData?.details;
|
|
268
|
+
const hint = errorData?.hint;
|
|
269
|
+
throw new ApiError(message, code, details, hint);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/features/chattemplate/shared/api/apiClient.ts
|
|
273
|
+
var SUPABASE_URL2 = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
274
|
+
if (!SUPABASE_URL2) {
|
|
275
|
+
throw new Error("Missing environment variable: NEXT_PUBLIC_SUPABASE_URL");
|
|
276
|
+
}
|
|
277
|
+
var BASE_URL = SUPABASE_URL2.endsWith("/") ? SUPABASE_URL2.slice(0, -1) : SUPABASE_URL2;
|
|
278
|
+
async function parseResponse(response) {
|
|
279
|
+
const text = await response.text();
|
|
280
|
+
return text ? JSON.parse(text) : {};
|
|
281
|
+
}
|
|
282
|
+
var apiClient = {
|
|
283
|
+
async get(path2, options) {
|
|
284
|
+
const headers = await getHeaders(options?.headers);
|
|
285
|
+
const response = await fetch(`${BASE_URL}${path2}`, {
|
|
286
|
+
method: "GET",
|
|
287
|
+
headers
|
|
288
|
+
});
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
await handleError(response);
|
|
291
|
+
}
|
|
292
|
+
return parseResponse(response);
|
|
293
|
+
},
|
|
294
|
+
async post(path2, body, options) {
|
|
295
|
+
const defaultHeaders = {
|
|
296
|
+
"Prefer": "return=representation"
|
|
297
|
+
};
|
|
298
|
+
const headers = await getHeaders({ ...defaultHeaders, ...options?.headers });
|
|
299
|
+
const response = await fetch(`${BASE_URL}${path2}`, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers,
|
|
302
|
+
body: JSON.stringify(body)
|
|
303
|
+
});
|
|
304
|
+
if (!response.ok) {
|
|
305
|
+
await handleError(response);
|
|
306
|
+
}
|
|
307
|
+
return parseResponse(response);
|
|
308
|
+
},
|
|
309
|
+
async patch(path2, body, options) {
|
|
310
|
+
const defaultHeaders = {
|
|
311
|
+
"Prefer": "return=representation"
|
|
312
|
+
};
|
|
313
|
+
const headers = await getHeaders({ ...defaultHeaders, ...options?.headers });
|
|
314
|
+
const response = await fetch(`${BASE_URL}${path2}`, {
|
|
315
|
+
method: "PATCH",
|
|
316
|
+
headers,
|
|
317
|
+
body: JSON.stringify(body)
|
|
318
|
+
});
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
await handleError(response);
|
|
321
|
+
}
|
|
322
|
+
return parseResponse(response);
|
|
323
|
+
},
|
|
324
|
+
async delete(path2, options) {
|
|
325
|
+
const defaultHeaders = {
|
|
326
|
+
"Prefer": "return=representation"
|
|
327
|
+
};
|
|
328
|
+
const headers = await getHeaders({ ...defaultHeaders, ...options?.headers });
|
|
329
|
+
const response = await fetch(`${BASE_URL}${path2}`, {
|
|
330
|
+
method: "DELETE",
|
|
331
|
+
headers
|
|
332
|
+
});
|
|
333
|
+
if (!response.ok) {
|
|
334
|
+
await handleError(response);
|
|
335
|
+
}
|
|
336
|
+
return parseResponse(response);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/features/chattemplate/shared/api/queryBuilder.ts
|
|
341
|
+
var QueryBuilder = class {
|
|
342
|
+
constructor() {
|
|
343
|
+
this.params = new URLSearchParams();
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Embeds resource relationships or filters output columns.
|
|
347
|
+
*/
|
|
348
|
+
select(fields) {
|
|
349
|
+
const cleaned = fields.replace(/\s+/g, "");
|
|
350
|
+
this.params.set("select", cleaned);
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Equality filter.
|
|
355
|
+
*/
|
|
356
|
+
eq(column, value) {
|
|
357
|
+
this.params.append(column, `eq.${value}`);
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
lt(column, value) {
|
|
361
|
+
this.params.append(column, `lt.${value}`);
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Case-insensitive pattern matching filter.
|
|
366
|
+
*/
|
|
367
|
+
ilike(column, value) {
|
|
368
|
+
this.params.append(column, `ilike.${value}`);
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* IN filter matching array of values.
|
|
373
|
+
*/
|
|
374
|
+
in(column, values) {
|
|
375
|
+
const formatted = values.map((v) => `${v}`).join(",");
|
|
376
|
+
this.params.append(column, `in.(${formatted})`);
|
|
377
|
+
return this;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Logical OR filter matching multiple column criteria.
|
|
381
|
+
* Example string: "email.ilike.test@gmail.com,contact_user_id.eq.some-uuid"
|
|
382
|
+
*/
|
|
383
|
+
or(filterString) {
|
|
384
|
+
this.params.set("or", `(${filterString})`);
|
|
385
|
+
return this;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Sorting filter.
|
|
389
|
+
*/
|
|
390
|
+
order(column, options) {
|
|
391
|
+
const dir = options?.ascending !== false ? "asc" : "desc";
|
|
392
|
+
this.params.set("order", `${column}.${dir}`);
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Limit result size.
|
|
397
|
+
*/
|
|
398
|
+
limit(n) {
|
|
399
|
+
this.params.set("limit", n.toString());
|
|
400
|
+
return this;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Offset result index.
|
|
404
|
+
*/
|
|
405
|
+
offset(n) {
|
|
406
|
+
this.params.set("offset", n.toString());
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Generates the final query parameter string (e.g. "?owner_id=eq.123").
|
|
411
|
+
*/
|
|
412
|
+
toString() {
|
|
413
|
+
const q = this.params.toString();
|
|
414
|
+
return q ? `?${q}` : "";
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
function createQuery() {
|
|
418
|
+
return new QueryBuilder();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/features/chattemplate/chat/api/messages.api.ts
|
|
422
|
+
async function createMessage(msg) {
|
|
423
|
+
try {
|
|
424
|
+
const memberQuery = createQuery().select("user_id").eq("conversation_id", msg.conversationId);
|
|
425
|
+
const members = await apiClient.get(
|
|
426
|
+
`/rest/v1/conversation_members${memberQuery.toString()}`
|
|
427
|
+
);
|
|
428
|
+
if (!members || members.length === 0) {
|
|
429
|
+
throw new Error("No members found in conversation");
|
|
430
|
+
}
|
|
431
|
+
const senderMsgId = msg.id || crypto.randomUUID();
|
|
432
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
433
|
+
let resolvedReplyToId = null;
|
|
434
|
+
if (msg.replyMetadata?.replyto_message_id) {
|
|
435
|
+
const parentMsgQuery = createQuery().select("id, sender_message_id").eq("id", msg.replyMetadata.replyto_message_id).limit(1);
|
|
436
|
+
const replyMsgs = await apiClient.get(
|
|
437
|
+
`/rest/v1/chat_messages${parentMsgQuery.toString()}`
|
|
438
|
+
);
|
|
439
|
+
const replyMsg = replyMsgs[0] || null;
|
|
440
|
+
resolvedReplyToId = replyMsg ? replyMsg.sender_message_id || replyMsg.id : msg.replyMetadata.replyto_message_id;
|
|
441
|
+
}
|
|
442
|
+
const records = [];
|
|
443
|
+
for (const member of members) {
|
|
444
|
+
const isSender = member.user_id === msg.senderId;
|
|
445
|
+
const msgId = isSender ? senderMsgId : crypto.randomUUID();
|
|
446
|
+
let finalMessage = msg.message;
|
|
447
|
+
if (msg.messageType === "system" && msg.systemMetadata) {
|
|
448
|
+
const { type: sysType, groupName, creatorName } = msg.systemMetadata;
|
|
449
|
+
if (sysType === "group_created") {
|
|
450
|
+
finalMessage = isSender ? `You created group "${groupName}"` : `${creatorName} created group "${groupName}"`;
|
|
451
|
+
} else if (sysType === "members_added") {
|
|
452
|
+
if (isSender) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
finalMessage = `${creatorName} added you`;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const hasPreparsedData = !!(msg.fileContentText || msg.fileContentJson);
|
|
459
|
+
records.push({
|
|
460
|
+
id: msgId,
|
|
461
|
+
conversation_id: msg.conversationId,
|
|
462
|
+
owner_user_id: member.user_id,
|
|
463
|
+
sender_user_id: msg.senderId,
|
|
464
|
+
message: finalMessage,
|
|
465
|
+
message_type: msg.messageType,
|
|
466
|
+
direction: isSender ? "Sent" : "Received",
|
|
467
|
+
sent: true,
|
|
468
|
+
received: isSender,
|
|
469
|
+
created_at: now,
|
|
470
|
+
file_url: msg.fileUrl || null,
|
|
471
|
+
file_name: msg.fileName || null,
|
|
472
|
+
file_size: msg.fileSize || null,
|
|
473
|
+
mime_type: msg.mimeType || null,
|
|
474
|
+
duration: msg.duration || null,
|
|
475
|
+
thumbnail: msg.thumbnail || null,
|
|
476
|
+
thumb: false,
|
|
477
|
+
favorite: false,
|
|
478
|
+
flag: false,
|
|
479
|
+
star: false,
|
|
480
|
+
pin: false,
|
|
481
|
+
archive: false,
|
|
482
|
+
deleted: false,
|
|
483
|
+
action_this: false,
|
|
484
|
+
reply: !!msg.replyMetadata,
|
|
485
|
+
forward: false,
|
|
486
|
+
replyemoji: msg.replyMetadata?.replyemoji || null,
|
|
487
|
+
replyto_message_id: resolvedReplyToId,
|
|
488
|
+
replyto_user_id: msg.replyMetadata?.replyto_user_id || null,
|
|
489
|
+
parent_message_id: msg.replyMetadata?.parent_message_id || null,
|
|
490
|
+
sender_message_id: isSender ? null : senderMsgId,
|
|
491
|
+
client_message_id: msg.clientMessageId || null,
|
|
492
|
+
message_status: "sent",
|
|
493
|
+
location_data: msg.locationData || null,
|
|
494
|
+
location_type: msg.locationType || null,
|
|
495
|
+
file_content_text: msg.fileContentText || null,
|
|
496
|
+
file_content_json: msg.fileContentJson || null,
|
|
497
|
+
processing_status: hasPreparsedData ? "completed" : msg.messageType === "document" && (msg.fileName?.toLowerCase().endsWith(".pdf") || msg.mimeType === "application/pdf") ? "pending" : null
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
if (records.length > 0) {
|
|
501
|
+
await apiClient.post("/rest/v1/chat_messages", records);
|
|
502
|
+
const isPdf = msg.messageType === "document" && (msg.fileName?.toLowerCase().endsWith(".pdf") || msg.mimeType === "application/pdf");
|
|
503
|
+
if (isPdf && typeof window !== "undefined") {
|
|
504
|
+
fetch("/api/process-pdf", {
|
|
505
|
+
method: "POST",
|
|
506
|
+
headers: { "Content-Type": "application/json" },
|
|
507
|
+
body: JSON.stringify({
|
|
508
|
+
messageId: senderMsgId
|
|
509
|
+
})
|
|
510
|
+
}).catch((err) => console.warn("[Messages API] Asynchronous PDF processing dispatch error:", err));
|
|
511
|
+
}
|
|
512
|
+
if (typeof window !== "undefined") {
|
|
513
|
+
fetch("/api/notifications/push", {
|
|
514
|
+
method: "POST",
|
|
515
|
+
headers: { "Content-Type": "application/json" },
|
|
516
|
+
body: JSON.stringify({
|
|
517
|
+
senderId: msg.senderId,
|
|
518
|
+
conversationId: msg.conversationId,
|
|
519
|
+
message: msg.message,
|
|
520
|
+
messageType: msg.messageType,
|
|
521
|
+
fileName: msg.fileName
|
|
522
|
+
})
|
|
523
|
+
}).catch((err) => console.warn("[Messages API] Asynchronous FCM push dispatch error:", err));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const senderRecord = records.find(
|
|
527
|
+
(r) => r.owner_user_id === msg.senderId
|
|
528
|
+
);
|
|
529
|
+
if (!senderRecord) return null;
|
|
530
|
+
const profileQuery = createQuery().select("id, name, email, avatar").eq("id", msg.senderId).limit(1);
|
|
531
|
+
const profiles = await apiClient.get(
|
|
532
|
+
`/rest/v1/profiles${profileQuery.toString()}`
|
|
533
|
+
);
|
|
534
|
+
const profile = profiles[0] || null;
|
|
535
|
+
return {
|
|
536
|
+
id: senderRecord.id,
|
|
537
|
+
conversation_id: senderRecord.conversation_id,
|
|
538
|
+
owner_user_id: senderRecord.owner_user_id,
|
|
539
|
+
sender_user_id: senderRecord.sender_user_id,
|
|
540
|
+
message: senderRecord.message,
|
|
541
|
+
message_type: senderRecord.message_type,
|
|
542
|
+
direction: senderRecord.direction,
|
|
543
|
+
sent: senderRecord.sent,
|
|
544
|
+
received: senderRecord.received,
|
|
545
|
+
created_at: senderRecord.created_at,
|
|
546
|
+
file_url: senderRecord.file_url || void 0,
|
|
547
|
+
file_name: senderRecord.file_name || void 0,
|
|
548
|
+
file_size: senderRecord.file_size ? Number(senderRecord.file_size) : void 0,
|
|
549
|
+
mime_type: senderRecord.mime_type || void 0,
|
|
550
|
+
duration: senderRecord.duration ? Number(senderRecord.duration) : void 0,
|
|
551
|
+
thumbnail: senderRecord.thumbnail || void 0,
|
|
552
|
+
thumb: senderRecord.thumb,
|
|
553
|
+
favorite: senderRecord.favorite,
|
|
554
|
+
flag: senderRecord.flag,
|
|
555
|
+
star: senderRecord.star,
|
|
556
|
+
pin: senderRecord.pin,
|
|
557
|
+
archive: senderRecord.archive,
|
|
558
|
+
deleted: senderRecord.deleted,
|
|
559
|
+
action_this: senderRecord.action_this,
|
|
560
|
+
reply: senderRecord.reply,
|
|
561
|
+
forward: senderRecord.forward,
|
|
562
|
+
replyemoji: senderRecord.replyemoji || void 0,
|
|
563
|
+
replyto_message_id: senderRecord.replyto_message_id || void 0,
|
|
564
|
+
replyto_user_id: senderRecord.replyto_user_id || void 0,
|
|
565
|
+
parent_message_id: senderRecord.parent_message_id || void 0,
|
|
566
|
+
message_status: senderRecord.message_status,
|
|
567
|
+
client_message_id: senderRecord.client_message_id || void 0,
|
|
568
|
+
location_data: senderRecord.location_data || void 0,
|
|
569
|
+
location_type: senderRecord.location_type || void 0,
|
|
570
|
+
file_content_text: senderRecord.file_content_text || void 0,
|
|
571
|
+
file_content_json: senderRecord.file_content_json || void 0,
|
|
572
|
+
processing_status: senderRecord.processing_status || void 0,
|
|
573
|
+
sender: profile ? {
|
|
574
|
+
id: profile.id,
|
|
575
|
+
name: profile.name || profile.email.split("@")[0],
|
|
576
|
+
email: profile.email,
|
|
577
|
+
avatar_url: profile.avatar || void 0
|
|
578
|
+
} : void 0
|
|
579
|
+
};
|
|
580
|
+
} catch (err) {
|
|
581
|
+
console.error("[Messages API] Failed to create message copies:", err);
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/features/chattemplate/chat/repositories/message-repository.ts
|
|
587
|
+
async function createMessage2(msg) {
|
|
588
|
+
return createMessage(msg);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/lib/db-alerts/types/db-alert.ts
|
|
592
|
+
var DB_ALERTS_CONFIG = {
|
|
593
|
+
groupName: "DB Alerts",
|
|
594
|
+
groupImage: "https://images.unsplash.com/photo-1598257006458-087169a1f08d?w=128&h=128&fit=crop&auto=format&q=80",
|
|
595
|
+
adminEmails: [
|
|
596
|
+
"itsaman00786@gmail.com",
|
|
597
|
+
"amanmicropay@gmail.com",
|
|
598
|
+
"n.rajukrishna@gmail.com"
|
|
599
|
+
]
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// src/services/db-alert.service.ts
|
|
603
|
+
function formatAlertTime(date) {
|
|
604
|
+
const day = date.getDate();
|
|
605
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
606
|
+
const month = months[date.getMonth()];
|
|
607
|
+
const year = date.getFullYear();
|
|
608
|
+
let hours = date.getHours();
|
|
609
|
+
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
610
|
+
const ampm = hours >= 12 ? "AM" : "PM";
|
|
611
|
+
hours = hours % 12;
|
|
612
|
+
hours = hours ? hours : 12;
|
|
613
|
+
return `${day} ${month} ${year} ${hours}:${minutes} ${ampm}`;
|
|
614
|
+
}
|
|
615
|
+
async function getOrCreateAlertConversation() {
|
|
616
|
+
const supabase2 = createClient();
|
|
617
|
+
try {
|
|
618
|
+
const { data: existing, error } = await supabase2.from("conversations").select("id").eq("name", DB_ALERTS_CONFIG.groupName).eq("type", "group").maybeSingle();
|
|
619
|
+
if (error) throw error;
|
|
620
|
+
if (existing) return existing.id;
|
|
621
|
+
const { data: newConvo, error: createError } = await supabase2.from("conversations").insert({
|
|
622
|
+
type: "group",
|
|
623
|
+
name: DB_ALERTS_CONFIG.groupName,
|
|
624
|
+
image: DB_ALERTS_CONFIG.groupImage || null
|
|
625
|
+
}).select("id").single();
|
|
626
|
+
if (createError) throw createError;
|
|
627
|
+
if (!newConvo) return null;
|
|
628
|
+
const { data: adminProfiles, error: profilesError } = await supabase2.from("profiles").select("id, email").in("email", DB_ALERTS_CONFIG.adminEmails);
|
|
629
|
+
if (profilesError) throw profilesError;
|
|
630
|
+
if (adminProfiles && adminProfiles.length > 0) {
|
|
631
|
+
const membersToInsert = adminProfiles.map((p) => ({
|
|
632
|
+
conversation_id: newConvo.id,
|
|
633
|
+
user_id: p.id,
|
|
634
|
+
role: "member"
|
|
635
|
+
}));
|
|
636
|
+
const { error: insertError } = await supabase2.from("conversation_members").insert(membersToInsert);
|
|
637
|
+
if (insertError) {
|
|
638
|
+
console.error("[DB Alerts] Failed to insert initial admin members:", insertError);
|
|
639
|
+
} else {
|
|
640
|
+
console.log(`[DB Alerts] Created group with ${adminProfiles.length} configured admin members`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return newConvo.id;
|
|
644
|
+
} catch (err) {
|
|
645
|
+
console.error("[DB Alerts] Error in getOrCreateAlertConversation:", err);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
var subscribedUsers = /* @__PURE__ */ new Set();
|
|
650
|
+
async function autoSubscribeAdmin(userId, email) {
|
|
651
|
+
if (subscribedUsers.has(userId)) return;
|
|
652
|
+
subscribedUsers.add(userId);
|
|
653
|
+
const emailLower = email.trim().toLowerCase();
|
|
654
|
+
const isAdmin = DB_ALERTS_CONFIG.adminEmails.some(
|
|
655
|
+
(adminEmail) => adminEmail.toLowerCase() === emailLower
|
|
656
|
+
);
|
|
657
|
+
if (!isAdmin) return;
|
|
658
|
+
const supabase2 = createClient();
|
|
659
|
+
try {
|
|
660
|
+
const convoId = await getOrCreateAlertConversation();
|
|
661
|
+
if (!convoId) return;
|
|
662
|
+
const { data: existing, error: checkError } = await supabase2.from("conversation_members").select("id").eq("conversation_id", convoId).eq("user_id", userId).maybeSingle();
|
|
663
|
+
if (checkError) throw checkError;
|
|
664
|
+
if (!existing) {
|
|
665
|
+
const { error: insertError } = await supabase2.from("conversation_members").insert({
|
|
666
|
+
conversation_id: convoId,
|
|
667
|
+
user_id: userId,
|
|
668
|
+
role: "member"
|
|
669
|
+
});
|
|
670
|
+
if (insertError) throw insertError;
|
|
671
|
+
console.log(`[DB Alerts] Auto-subscribed admin user: ${email} (${userId})`);
|
|
672
|
+
}
|
|
673
|
+
const { data: msgs, error: msgsError } = await supabase2.from("chat_messages").select("id").eq("conversation_id", convoId).eq("owner_user_id", userId).limit(1);
|
|
674
|
+
if (!msgsError && (!msgs || msgs.length === 0)) {
|
|
675
|
+
await createMessage2({
|
|
676
|
+
conversationId: convoId,
|
|
677
|
+
senderId: userId,
|
|
678
|
+
message: "\u{1F6A8} DB Alerts Channel Initialized. Administrative events will be logged here.",
|
|
679
|
+
messageType: "system"
|
|
680
|
+
});
|
|
681
|
+
console.log(`[DB Alerts] Initialized message feed for user: ${userId}`);
|
|
682
|
+
}
|
|
683
|
+
} catch (err) {
|
|
684
|
+
console.error("[DB Alerts] Failed to auto-subscribe admin user:", err);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function triggerContactAlert(action, ownerId, contactUserId) {
|
|
688
|
+
const supabase2 = createClient();
|
|
689
|
+
try {
|
|
690
|
+
const convoId = await getOrCreateAlertConversation();
|
|
691
|
+
if (!convoId) return;
|
|
692
|
+
const { data: ownerProfile } = await supabase2.from("profiles").select("name, email").eq("id", ownerId).maybeSingle();
|
|
693
|
+
const ownerName = ownerProfile?.name || ownerProfile?.email?.split("@")[0] || "System";
|
|
694
|
+
const { data: contactProfile } = await supabase2.from("profiles").select("name, email").eq("id", contactUserId).maybeSingle();
|
|
695
|
+
const contactName = contactProfile?.name || contactProfile?.email?.split("@")[0] || "System";
|
|
696
|
+
const timeStr = formatAlertTime(/* @__PURE__ */ new Date());
|
|
697
|
+
let formattedMessage = "";
|
|
698
|
+
if (action === "create") {
|
|
699
|
+
formattedMessage = `Contact Created
|
|
700
|
+
\u{1F7E2} Contact Added
|
|
701
|
+
By: ${ownerName}
|
|
702
|
+
Contact: ${contactName}
|
|
703
|
+
Time: ${timeStr}`;
|
|
704
|
+
} else if (action === "delete") {
|
|
705
|
+
formattedMessage = `Contact Deleted
|
|
706
|
+
\u{1F534} Contact Deleted
|
|
707
|
+
By: ${ownerName}
|
|
708
|
+
Contact: ${contactName}`;
|
|
709
|
+
} else if (action === "update") {
|
|
710
|
+
formattedMessage = `Contact Updated
|
|
711
|
+
\u{1F7E1} Contact Updated
|
|
712
|
+
By: ${ownerName}
|
|
713
|
+
Contact: ${contactName}
|
|
714
|
+
Time: ${timeStr}`;
|
|
715
|
+
}
|
|
716
|
+
await createMessage2({
|
|
717
|
+
conversationId: convoId,
|
|
718
|
+
senderId: ownerId,
|
|
719
|
+
message: formattedMessage,
|
|
720
|
+
messageType: "system"
|
|
721
|
+
});
|
|
722
|
+
} catch (err) {
|
|
723
|
+
console.error("[DB Alerts] Failed to trigger contact alert:", err);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function triggerGroupAlert(action, actorId, groupName) {
|
|
727
|
+
const supabase2 = createClient();
|
|
728
|
+
try {
|
|
729
|
+
const convoId = await getOrCreateAlertConversation();
|
|
730
|
+
if (!convoId) return;
|
|
731
|
+
let actorName = "System";
|
|
732
|
+
if (actorId) {
|
|
733
|
+
const { data: actorProfile } = await supabase2.from("profiles").select("name, email").eq("id", actorId).maybeSingle();
|
|
734
|
+
actorName = actorProfile?.name || actorProfile?.email?.split("@")[0] || "System";
|
|
735
|
+
}
|
|
736
|
+
let formattedMessage = "";
|
|
737
|
+
if (action === "create") {
|
|
738
|
+
formattedMessage = `Group Created
|
|
739
|
+
\u{1F7E2} Group Created
|
|
740
|
+
By: ${actorName}
|
|
741
|
+
Group: ${groupName}`;
|
|
742
|
+
} else if (action === "delete") {
|
|
743
|
+
formattedMessage = `Group Deleted
|
|
744
|
+
\u{1F534} Group Deleted
|
|
745
|
+
By: ${actorName}
|
|
746
|
+
Group: ${groupName}`;
|
|
747
|
+
} else if (action === "update") {
|
|
748
|
+
formattedMessage = `Group Updated
|
|
749
|
+
\u{1F7E1} Group Updated
|
|
750
|
+
By: ${actorName}
|
|
751
|
+
Group: ${groupName}`;
|
|
752
|
+
}
|
|
753
|
+
let senderId = actorId;
|
|
754
|
+
if (!senderId) {
|
|
755
|
+
const { data: firstMember } = await supabase2.from("conversation_members").select("user_id").eq("conversation_id", convoId).limit(1).maybeSingle();
|
|
756
|
+
senderId = firstMember?.user_id || "";
|
|
757
|
+
}
|
|
758
|
+
if (!senderId) {
|
|
759
|
+
console.warn("[DB Alerts] Cannot send alert because no valid sender ID is available.");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
await createMessage2({
|
|
763
|
+
conversationId: convoId,
|
|
764
|
+
senderId,
|
|
765
|
+
message: formattedMessage,
|
|
766
|
+
messageType: "system"
|
|
767
|
+
});
|
|
768
|
+
} catch (err) {
|
|
769
|
+
console.error("[DB Alerts] Failed to trigger group alert:", err);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/services/file.service.ts
|
|
774
|
+
function parseFileUrl(fileUrl) {
|
|
775
|
+
try {
|
|
776
|
+
const url = new URL(fileUrl);
|
|
777
|
+
const pathParts = url.pathname.split("/");
|
|
778
|
+
const publicIndex = pathParts.indexOf("public");
|
|
779
|
+
if (publicIndex !== -1 && publicIndex + 2 < pathParts.length) {
|
|
780
|
+
const bucket = pathParts[publicIndex + 1];
|
|
781
|
+
const storagePath = pathParts.slice(publicIndex + 2).join("/");
|
|
782
|
+
return { bucket, storagePath };
|
|
783
|
+
}
|
|
784
|
+
const authenticatedIndex = pathParts.indexOf("authenticated");
|
|
785
|
+
if (authenticatedIndex !== -1 && authenticatedIndex + 2 < pathParts.length) {
|
|
786
|
+
const bucket = pathParts[authenticatedIndex + 1];
|
|
787
|
+
const storagePath = pathParts.slice(authenticatedIndex + 2).join("/");
|
|
788
|
+
return { bucket, storagePath };
|
|
789
|
+
}
|
|
790
|
+
const chatFilesIndex = pathParts.indexOf("chat-files");
|
|
791
|
+
if (chatFilesIndex !== -1 && chatFilesIndex + 1 < pathParts.length) {
|
|
792
|
+
return { bucket: "chat-files", storagePath: pathParts.slice(chatFilesIndex + 1).join("/") };
|
|
793
|
+
}
|
|
794
|
+
} catch (e) {
|
|
795
|
+
console.error("Failed to parse file URL:", e);
|
|
796
|
+
}
|
|
797
|
+
return { bucket: "chat-files", storagePath: "" };
|
|
798
|
+
}
|
|
799
|
+
async function getSharedFileMetadata(supabase2, fileId, userId) {
|
|
800
|
+
console.log(`[DEBUG server] getSharedFileMetadata \u2192 fileId: ${fileId}, userId: ${userId}`);
|
|
801
|
+
try {
|
|
802
|
+
const { data, error } = await supabase2.rpc("get_shared_file_metadata", { p_file_id: fileId, p_user_id: userId }).maybeSingle();
|
|
803
|
+
if (error) {
|
|
804
|
+
console.error(`[DEBUG server] RPC error:`, error.message, error);
|
|
805
|
+
return { success: false, error: error.message };
|
|
806
|
+
}
|
|
807
|
+
if (!data) {
|
|
808
|
+
console.warn(`[DEBUG server] RPC returned null \u2192 file not found or user is not a conversation member. fileId: ${fileId}, userId: ${userId}`);
|
|
809
|
+
return { success: false, error: "File not found or unauthorized" };
|
|
810
|
+
}
|
|
811
|
+
console.log(`[DEBUG server] RPC success \u2192 conversationId: ${data.out_conversation_id}, file: ${data.out_file_name}`);
|
|
812
|
+
const parsed = parseFileUrl(data.out_file_url);
|
|
813
|
+
return {
|
|
814
|
+
success: true,
|
|
815
|
+
data: {
|
|
816
|
+
id: fileId,
|
|
817
|
+
bucket: parsed.bucket,
|
|
818
|
+
storagePath: parsed.storagePath,
|
|
819
|
+
fileName: data.out_file_name || "document",
|
|
820
|
+
fileSize: data.out_file_size,
|
|
821
|
+
mimeType: data.out_mime_type || "application/octet-stream",
|
|
822
|
+
conversationId: data.out_conversation_id,
|
|
823
|
+
ownerId: data.out_sender_user_id || "",
|
|
824
|
+
createdAt: data.out_created_at
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
} catch (err) {
|
|
828
|
+
console.error(`[DEBUG server] Exception in getSharedFileMetadata:`, err.message || err);
|
|
829
|
+
return { success: false, error: err.message || "Internal server error" };
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function generateSignedUrl(supabase2, bucket, storagePath) {
|
|
833
|
+
const { data, error } = await supabase2.storage.from(bucket).createSignedUrl(storagePath, 60);
|
|
834
|
+
if (error || !data) {
|
|
835
|
+
throw new Error(error?.message || "Failed to generate signed URL");
|
|
836
|
+
}
|
|
837
|
+
return data.signedUrl;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/utils/pdf/createPdf.ts
|
|
841
|
+
import { PDFDocument, PageSizes } from "pdf-lib";
|
|
842
|
+
|
|
843
|
+
// src/constants/scanner.ts
|
|
844
|
+
var DEFAULT_JPEG_QUALITY = 0.85;
|
|
845
|
+
var FALLBACK_CROP_MARGIN_RATIO = 0.05;
|
|
846
|
+
|
|
847
|
+
// src/utils/scanner/compress.ts
|
|
848
|
+
function compressCanvasToJpeg(sourceCanvas, maxDimension = 2e3, quality = DEFAULT_JPEG_QUALITY) {
|
|
849
|
+
let width = sourceCanvas.width;
|
|
850
|
+
let height = sourceCanvas.height;
|
|
851
|
+
if (width > maxDimension || height > maxDimension) {
|
|
852
|
+
if (width > height) {
|
|
853
|
+
height = Math.round(height * maxDimension / width);
|
|
854
|
+
width = maxDimension;
|
|
855
|
+
} else {
|
|
856
|
+
width = Math.round(width * maxDimension / height);
|
|
857
|
+
height = maxDimension;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const outCanvas = document.createElement("canvas");
|
|
861
|
+
outCanvas.width = width;
|
|
862
|
+
outCanvas.height = height;
|
|
863
|
+
const ctx = outCanvas.getContext("2d");
|
|
864
|
+
if (ctx) {
|
|
865
|
+
ctx.imageSmoothingEnabled = true;
|
|
866
|
+
ctx.imageSmoothingQuality = "high";
|
|
867
|
+
ctx.drawImage(sourceCanvas, 0, 0, width, height);
|
|
868
|
+
}
|
|
869
|
+
return outCanvas.toDataURL("image/jpeg", quality);
|
|
870
|
+
}
|
|
871
|
+
function dataUrlToUint8Array(dataUrl) {
|
|
872
|
+
const base64Str = dataUrl.split(",")[1] || dataUrl;
|
|
873
|
+
const binaryStr = atob(base64Str);
|
|
874
|
+
const len = binaryStr.length;
|
|
875
|
+
const bytes = new Uint8Array(len);
|
|
876
|
+
for (let i = 0; i < len; i++) {
|
|
877
|
+
bytes[i] = binaryStr.charCodeAt(i);
|
|
878
|
+
}
|
|
879
|
+
return bytes;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/utils/pdf/createPdf.ts
|
|
883
|
+
async function createPdfFromScanPages(pages, options = {}) {
|
|
884
|
+
const {
|
|
885
|
+
paperSize = "a4",
|
|
886
|
+
orientation = "portrait",
|
|
887
|
+
quality = 0.85
|
|
888
|
+
} = options;
|
|
889
|
+
const pdfDoc = await PDFDocument.create();
|
|
890
|
+
for (const page of pages) {
|
|
891
|
+
const imgCanvas = await renderPageToRotatedCanvas(page);
|
|
892
|
+
const jpegDataUrl = compressCanvasToJpeg(imgCanvas, 2e3, quality);
|
|
893
|
+
const imageBytes = dataUrlToUint8Array(jpegDataUrl);
|
|
894
|
+
const embeddedImage = await pdfDoc.embedJpg(imageBytes);
|
|
895
|
+
let pageWidth = embeddedImage.width;
|
|
896
|
+
let pageHeight = embeddedImage.height;
|
|
897
|
+
if (paperSize === "a4") {
|
|
898
|
+
const a4 = PageSizes.A4;
|
|
899
|
+
if (orientation === "landscape") {
|
|
900
|
+
pageWidth = a4[1];
|
|
901
|
+
pageHeight = a4[0];
|
|
902
|
+
} else {
|
|
903
|
+
pageWidth = a4[0];
|
|
904
|
+
pageHeight = a4[1];
|
|
905
|
+
}
|
|
906
|
+
} else if (orientation === "landscape" && pageWidth < pageHeight) {
|
|
907
|
+
const tmp = pageWidth;
|
|
908
|
+
pageWidth = pageHeight;
|
|
909
|
+
pageHeight = tmp;
|
|
910
|
+
}
|
|
911
|
+
const pdfPage = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
912
|
+
const scale = Math.min(
|
|
913
|
+
pageWidth / embeddedImage.width,
|
|
914
|
+
pageHeight / embeddedImage.height
|
|
915
|
+
);
|
|
916
|
+
const drawW = embeddedImage.width * scale;
|
|
917
|
+
const drawH = embeddedImage.height * scale;
|
|
918
|
+
const drawX = (pageWidth - drawW) / 2;
|
|
919
|
+
const drawY = (pageHeight - drawH) / 2;
|
|
920
|
+
pdfPage.drawImage(embeddedImage, {
|
|
921
|
+
x: drawX,
|
|
922
|
+
y: drawY,
|
|
923
|
+
width: drawW,
|
|
924
|
+
height: drawH
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
return await pdfDoc.save();
|
|
928
|
+
}
|
|
929
|
+
async function renderPageToRotatedCanvas(page) {
|
|
930
|
+
return new Promise((resolve, reject) => {
|
|
931
|
+
const img = new Image();
|
|
932
|
+
img.crossOrigin = "anonymous";
|
|
933
|
+
img.onload = () => {
|
|
934
|
+
const rot = (page.rotation % 360 + 360) % 360;
|
|
935
|
+
const is90or270 = rot === 90 || rot === 270;
|
|
936
|
+
const w = is90or270 ? img.height : img.width;
|
|
937
|
+
const h = is90or270 ? img.width : img.height;
|
|
938
|
+
const canvas = document.createElement("canvas");
|
|
939
|
+
canvas.width = w;
|
|
940
|
+
canvas.height = h;
|
|
941
|
+
const ctx = canvas.getContext("2d");
|
|
942
|
+
if (!ctx) {
|
|
943
|
+
reject(new Error("Failed to get 2d context for PDF page render"));
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
ctx.translate(w / 2, h / 2);
|
|
947
|
+
ctx.rotate(rot * Math.PI / 180);
|
|
948
|
+
ctx.drawImage(img, -img.width / 2, -img.height / 2);
|
|
949
|
+
resolve(canvas);
|
|
950
|
+
};
|
|
951
|
+
img.onerror = (err) => reject(err);
|
|
952
|
+
img.src = page.processedUrl || page.originalUrl;
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/services/pdf.service.ts
|
|
957
|
+
var PdfService = class {
|
|
958
|
+
/**
|
|
959
|
+
* Generate a PDF Uint8Array buffer from scan pages.
|
|
960
|
+
*
|
|
961
|
+
* @param pages Array of ScanPage items
|
|
962
|
+
* @param options PDF compilation parameters
|
|
963
|
+
* @returns Uint8Array PDF byte stream
|
|
964
|
+
*/
|
|
965
|
+
async generatePdfBuffer(pages, options = {}) {
|
|
966
|
+
if (!pages || pages.length === 0) {
|
|
967
|
+
throw new Error("Cannot generate PDF with 0 pages");
|
|
968
|
+
}
|
|
969
|
+
return await createPdfFromScanPages(pages, options);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Create a File object containing PDF binary data ready for uploading.
|
|
973
|
+
*
|
|
974
|
+
* @param pages Array of ScanPage items
|
|
975
|
+
* @param filename Desired PDF filename
|
|
976
|
+
* @param options PDF settings
|
|
977
|
+
* @returns Promise resolving to a File instance (`application/pdf`)
|
|
978
|
+
*/
|
|
979
|
+
async generatePdfFile(pages, filename = `scanned_doc_${Date.now()}.pdf`, options = {}) {
|
|
980
|
+
const pdfBuffer = await this.generatePdfBuffer(pages, options);
|
|
981
|
+
const cleanFilename = filename.endsWith(".pdf") ? filename : `${filename}.pdf`;
|
|
982
|
+
return new File([pdfBuffer.buffer], cleanFilename, { type: "application/pdf" });
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Create a browser Blob preview URL from PDF bytes.
|
|
986
|
+
*
|
|
987
|
+
* @param pdfBuffer Uint8Array PDF data
|
|
988
|
+
* @returns Object URL string (`blob:http...`)
|
|
989
|
+
*/
|
|
990
|
+
createPdfPreviewUrl(pdfBuffer) {
|
|
991
|
+
const blob = new Blob([pdfBuffer.buffer], { type: "application/pdf" });
|
|
992
|
+
return URL.createObjectURL(blob);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Revoke blob URL to release browser memory.
|
|
996
|
+
*
|
|
997
|
+
* @param url Blob URL to revoke
|
|
998
|
+
*/
|
|
999
|
+
revokePdfPreviewUrl(url) {
|
|
1000
|
+
if (url && url.startsWith("blob:")) {
|
|
1001
|
+
URL.revokeObjectURL(url);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
var pdfService = new PdfService();
|
|
1006
|
+
|
|
1007
|
+
// src/services/opencv.service.ts
|
|
1008
|
+
var OpenCVService = class _OpenCVService {
|
|
1009
|
+
constructor() {
|
|
1010
|
+
this.isLoaded = false;
|
|
1011
|
+
this.isLoading = false;
|
|
1012
|
+
this.loadPromise = null;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Singleton instance accessor.
|
|
1016
|
+
*/
|
|
1017
|
+
static getInstance() {
|
|
1018
|
+
if (!_OpenCVService.instance) {
|
|
1019
|
+
_OpenCVService.instance = new _OpenCVService();
|
|
1020
|
+
}
|
|
1021
|
+
return _OpenCVService.instance;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Check if OpenCV.js is fully loaded and ready for execution.
|
|
1025
|
+
*/
|
|
1026
|
+
isReady() {
|
|
1027
|
+
return this.isLoaded && typeof window !== "undefined" && !!window.cv;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Lazy load OpenCV.js script asynchronously with single-flight promise deduplication.
|
|
1031
|
+
*/
|
|
1032
|
+
loadOpenCV() {
|
|
1033
|
+
console.log("[OpenCV] OpenCV script load disabled to ensure main-thread responsiveness. Falling back to native Canvas engine.");
|
|
1034
|
+
return Promise.resolve(null);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Safely dispose an array of OpenCV.js Mat structures to prevent WebAssembly memory leaks.
|
|
1038
|
+
*/
|
|
1039
|
+
safeDelete(...mats) {
|
|
1040
|
+
for (const mat of mats) {
|
|
1041
|
+
if (mat && typeof mat.delete === "function") {
|
|
1042
|
+
try {
|
|
1043
|
+
mat.delete();
|
|
1044
|
+
} catch (e) {
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
var opencvService = OpenCVService.getInstance();
|
|
1051
|
+
|
|
1052
|
+
// src/utils/scanner/detectEdges.ts
|
|
1053
|
+
function detectDocumentEdges(imageSource, cv) {
|
|
1054
|
+
const width = "naturalWidth" in imageSource ? imageSource.naturalWidth : imageSource.width;
|
|
1055
|
+
const height = "naturalHeight" in imageSource ? imageSource.naturalHeight : imageSource.height;
|
|
1056
|
+
const fallbackQuad = getFallbackCropQuad();
|
|
1057
|
+
if (!width || !height) return fallbackQuad;
|
|
1058
|
+
if (cv && cv.Mat) {
|
|
1059
|
+
try {
|
|
1060
|
+
const srcMat = cv.imread(imageSource);
|
|
1061
|
+
const grayMat = new cv.Mat();
|
|
1062
|
+
const blurMat = new cv.Mat();
|
|
1063
|
+
const cannyMat = new cv.Mat();
|
|
1064
|
+
cv.cvtColor(srcMat, grayMat, cv.COLOR_RGBA2GRAY, 0);
|
|
1065
|
+
cv.GaussianBlur(grayMat, blurMat, new cv.Size(5, 5), 0, 0, cv.BORDER_DEFAULT);
|
|
1066
|
+
cv.Canny(blurMat, cannyMat, 75, 200);
|
|
1067
|
+
const contours = new cv.MatVector();
|
|
1068
|
+
const hierarchy = new cv.Mat();
|
|
1069
|
+
cv.findContours(cannyMat, contours, hierarchy, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE);
|
|
1070
|
+
let maxArea = 0;
|
|
1071
|
+
let maxContour = null;
|
|
1072
|
+
for (let i = 0; i < contours.size(); ++i) {
|
|
1073
|
+
const cnt = contours.get(i);
|
|
1074
|
+
const area = cv.contourArea(cnt);
|
|
1075
|
+
if (area > width * height * 0.15) {
|
|
1076
|
+
const peri = cv.arcLength(cnt, true);
|
|
1077
|
+
const approx = new cv.Mat();
|
|
1078
|
+
cv.approxPolyDP(cnt, approx, 0.02 * peri, true);
|
|
1079
|
+
if (approx.rows === 4 && area > maxArea) {
|
|
1080
|
+
maxArea = area;
|
|
1081
|
+
if (maxContour) maxContour.delete();
|
|
1082
|
+
maxContour = approx;
|
|
1083
|
+
} else {
|
|
1084
|
+
approx.delete();
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
cnt.delete();
|
|
1088
|
+
}
|
|
1089
|
+
let detectedQuad = null;
|
|
1090
|
+
if (maxContour && maxContour.rows === 4) {
|
|
1091
|
+
const points = [];
|
|
1092
|
+
for (let i = 0; i < 4; i++) {
|
|
1093
|
+
const pt = maxContour.data32S;
|
|
1094
|
+
points.push({
|
|
1095
|
+
x: pt[i * 2] / width,
|
|
1096
|
+
y: pt[i * 2 + 1] / height
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
detectedQuad = sortQuadPoints(points);
|
|
1100
|
+
}
|
|
1101
|
+
srcMat.delete();
|
|
1102
|
+
grayMat.delete();
|
|
1103
|
+
blurMat.delete();
|
|
1104
|
+
cannyMat.delete();
|
|
1105
|
+
contours.delete();
|
|
1106
|
+
hierarchy.delete();
|
|
1107
|
+
if (maxContour) maxContour.delete();
|
|
1108
|
+
if (detectedQuad) return detectedQuad;
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
console.warn("[detectEdges] OpenCV detection failed, falling back to margin quad:", error);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return fallbackQuad;
|
|
1114
|
+
}
|
|
1115
|
+
function getFallbackCropQuad() {
|
|
1116
|
+
const m = FALLBACK_CROP_MARGIN_RATIO;
|
|
1117
|
+
return {
|
|
1118
|
+
topLeft: { x: m, y: m },
|
|
1119
|
+
topRight: { x: 1 - m, y: m },
|
|
1120
|
+
bottomRight: { x: 1 - m, y: 1 - m },
|
|
1121
|
+
bottomLeft: { x: m, y: 1 - m }
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function sortQuadPoints(pts) {
|
|
1125
|
+
const sortedBySum = [...pts].sort((a, b) => a.x + a.y - (b.x + b.y));
|
|
1126
|
+
const topLeft = sortedBySum[0];
|
|
1127
|
+
const bottomRight = sortedBySum[3];
|
|
1128
|
+
const remaining = [sortedBySum[1], sortedBySum[2]];
|
|
1129
|
+
const sortedByDiff = remaining.sort((a, b) => a.y - a.x - (b.y - b.x));
|
|
1130
|
+
const topRight = sortedByDiff[0];
|
|
1131
|
+
const bottomLeft = sortedByDiff[1];
|
|
1132
|
+
return { topLeft, topRight, bottomRight, bottomLeft };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/utils/scanner/perspective.ts
|
|
1136
|
+
function applyPerspectiveTransform(sourceCanvas, quad, cv) {
|
|
1137
|
+
const w = sourceCanvas.width;
|
|
1138
|
+
const h = sourceCanvas.height;
|
|
1139
|
+
const pTL = { x: quad.topLeft.x * w, y: quad.topLeft.y * h };
|
|
1140
|
+
const pTR = { x: quad.topRight.x * w, y: quad.topRight.y * h };
|
|
1141
|
+
const pBR = { x: quad.bottomRight.x * w, y: quad.bottomRight.y * h };
|
|
1142
|
+
const pBL = { x: quad.bottomLeft.x * w, y: quad.bottomLeft.y * h };
|
|
1143
|
+
const widthA = Math.hypot(pBR.x - pBL.x, pBR.y - pBL.y);
|
|
1144
|
+
const widthB = Math.hypot(pTR.x - pTL.x, pTR.y - pTL.y);
|
|
1145
|
+
const targetWidth = Math.max(100, Math.round(Math.max(widthA, widthB)));
|
|
1146
|
+
const heightA = Math.hypot(pTR.x - pBR.x, pTR.y - pBR.y);
|
|
1147
|
+
const heightB = Math.hypot(pTL.x - pBL.x, pTL.y - pBL.y);
|
|
1148
|
+
const targetHeight = Math.max(100, Math.round(Math.max(heightA, heightB)));
|
|
1149
|
+
const outCanvas = document.createElement("canvas");
|
|
1150
|
+
outCanvas.width = targetWidth;
|
|
1151
|
+
outCanvas.height = targetHeight;
|
|
1152
|
+
if (cv && cv.Mat) {
|
|
1153
|
+
try {
|
|
1154
|
+
const srcMat = cv.imread(sourceCanvas);
|
|
1155
|
+
const dstMat = new cv.Mat();
|
|
1156
|
+
const srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
1157
|
+
pTL.x,
|
|
1158
|
+
pTL.y,
|
|
1159
|
+
pTR.x,
|
|
1160
|
+
pTR.y,
|
|
1161
|
+
pBR.x,
|
|
1162
|
+
pBR.y,
|
|
1163
|
+
pBL.x,
|
|
1164
|
+
pBL.y
|
|
1165
|
+
]);
|
|
1166
|
+
const dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
1167
|
+
0,
|
|
1168
|
+
0,
|
|
1169
|
+
targetWidth,
|
|
1170
|
+
0,
|
|
1171
|
+
targetWidth,
|
|
1172
|
+
targetHeight,
|
|
1173
|
+
0,
|
|
1174
|
+
targetHeight
|
|
1175
|
+
]);
|
|
1176
|
+
const M = cv.getPerspectiveTransform(srcTri, dstTri);
|
|
1177
|
+
const dsize = new cv.Size(targetWidth, targetHeight);
|
|
1178
|
+
cv.warpPerspective(srcMat, dstMat, M, dsize, cv.INTER_LINEAR, cv.BORDER_CONSTANT, new cv.Scalar());
|
|
1179
|
+
cv.imshow(outCanvas, dstMat);
|
|
1180
|
+
srcMat.delete();
|
|
1181
|
+
dstMat.delete();
|
|
1182
|
+
srcTri.delete();
|
|
1183
|
+
dstTri.delete();
|
|
1184
|
+
M.delete();
|
|
1185
|
+
return outCanvas;
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
console.warn("[perspective] OpenCV homography failed, using fallback canvas clip:", err);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
const ctx = outCanvas.getContext("2d");
|
|
1191
|
+
if (ctx) {
|
|
1192
|
+
const minX = Math.min(pTL.x, pTR.x, pBR.x, pBL.x);
|
|
1193
|
+
const minY = Math.min(pTL.y, pTR.y, pBR.y, pBL.y);
|
|
1194
|
+
const cropW = Math.max(1, Math.max(pTL.x, pTR.x, pBR.x, pBL.x) - minX);
|
|
1195
|
+
const cropH = Math.max(1, Math.max(pTL.y, pTR.y, pBR.y, pBL.y) - minY);
|
|
1196
|
+
ctx.drawImage(sourceCanvas, minX, minY, cropW, cropH, 0, 0, targetWidth, targetHeight);
|
|
1197
|
+
}
|
|
1198
|
+
return outCanvas;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/utils/scanner/enhance.ts
|
|
1202
|
+
function applyEnhancementFilter(canvas, filter, brightness = 0, contrast = 0, cv) {
|
|
1203
|
+
const outCanvas = document.createElement("canvas");
|
|
1204
|
+
outCanvas.width = canvas.width;
|
|
1205
|
+
outCanvas.height = canvas.height;
|
|
1206
|
+
const ctx = outCanvas.getContext("2d");
|
|
1207
|
+
if (!ctx) return canvas;
|
|
1208
|
+
ctx.drawImage(canvas, 0, 0);
|
|
1209
|
+
if (cv && cv.Mat && filter !== "original") {
|
|
1210
|
+
try {
|
|
1211
|
+
const srcMat = cv.imread(outCanvas);
|
|
1212
|
+
const dstMat = new cv.Mat();
|
|
1213
|
+
if (filter === "grayscale") {
|
|
1214
|
+
cv.cvtColor(srcMat, dstMat, cv.COLOR_RGBA2GRAY, 0);
|
|
1215
|
+
cv.cvtColor(dstMat, dstMat, cv.COLOR_GRAY2RGBA, 0);
|
|
1216
|
+
} else if (filter === "bw") {
|
|
1217
|
+
const grayMat = new cv.Mat();
|
|
1218
|
+
cv.cvtColor(srcMat, grayMat, cv.COLOR_RGBA2GRAY, 0);
|
|
1219
|
+
cv.adaptiveThreshold(
|
|
1220
|
+
grayMat,
|
|
1221
|
+
grayMat,
|
|
1222
|
+
255,
|
|
1223
|
+
cv.ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
1224
|
+
cv.THRESH_BINARY,
|
|
1225
|
+
21,
|
|
1226
|
+
10
|
|
1227
|
+
);
|
|
1228
|
+
cv.cvtColor(grayMat, dstMat, cv.COLOR_GRAY2RGBA, 0);
|
|
1229
|
+
grayMat.delete();
|
|
1230
|
+
} else if (filter === "enhanced") {
|
|
1231
|
+
const labMat = new cv.Mat();
|
|
1232
|
+
cv.cvtColor(srcMat, labMat, cv.COLOR_RGBA2RGB, 0);
|
|
1233
|
+
cv.cvtColor(labMat, labMat, cv.COLOR_RGB2Lab, 0);
|
|
1234
|
+
const channels = new cv.MatVector();
|
|
1235
|
+
cv.split(labMat, channels);
|
|
1236
|
+
const lChannel = channels.get(0);
|
|
1237
|
+
const clahe = new cv.CLAHE(2, new cv.Size(8, 8));
|
|
1238
|
+
clahe.apply(lChannel, lChannel);
|
|
1239
|
+
channels.set(0, lChannel);
|
|
1240
|
+
cv.merge(channels, labMat);
|
|
1241
|
+
cv.cvtColor(labMat, dstMat, cv.COLOR_Lab2RGBA, 0);
|
|
1242
|
+
labMat.delete();
|
|
1243
|
+
channels.delete();
|
|
1244
|
+
lChannel.delete();
|
|
1245
|
+
clahe.delete();
|
|
1246
|
+
}
|
|
1247
|
+
cv.imshow(outCanvas, dstMat);
|
|
1248
|
+
srcMat.delete();
|
|
1249
|
+
dstMat.delete();
|
|
1250
|
+
} catch (e) {
|
|
1251
|
+
console.warn("[enhance] OpenCV enhancement failed, fallback to canvas pixel math:", e);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const imgData = ctx.getImageData(0, 0, outCanvas.width, outCanvas.height);
|
|
1255
|
+
const data = imgData.data;
|
|
1256
|
+
const bFactor = brightness * 2.55;
|
|
1257
|
+
const cFactor = 259 * (contrast + 255) / (255 * (259 - contrast));
|
|
1258
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1259
|
+
let r = data[i];
|
|
1260
|
+
let g = data[i + 1];
|
|
1261
|
+
let b = data[i + 2];
|
|
1262
|
+
if (!cv && filter === "grayscale") {
|
|
1263
|
+
const avg = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
1264
|
+
r = avg;
|
|
1265
|
+
g = avg;
|
|
1266
|
+
b = avg;
|
|
1267
|
+
} else if (!cv && filter === "bw") {
|
|
1268
|
+
const avg = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
1269
|
+
const bw = avg > 128 ? 255 : 0;
|
|
1270
|
+
r = bw;
|
|
1271
|
+
g = bw;
|
|
1272
|
+
b = bw;
|
|
1273
|
+
} else if (!cv && filter === "enhanced") {
|
|
1274
|
+
r = Math.min(255, r * 1.1 + 10);
|
|
1275
|
+
g = Math.min(255, g * 1.1 + 10);
|
|
1276
|
+
b = Math.min(255, b * 1.1 + 10);
|
|
1277
|
+
}
|
|
1278
|
+
if (brightness !== 0 || contrast !== 0) {
|
|
1279
|
+
r = cFactor * (r - 128) + 128 + bFactor;
|
|
1280
|
+
g = cFactor * (g - 128) + 128 + bFactor;
|
|
1281
|
+
b = cFactor * (b - 128) + 128 + bFactor;
|
|
1282
|
+
}
|
|
1283
|
+
data[i] = Math.min(255, Math.max(0, r));
|
|
1284
|
+
data[i + 1] = Math.min(255, Math.max(0, g));
|
|
1285
|
+
data[i + 2] = Math.min(255, Math.max(0, b));
|
|
1286
|
+
}
|
|
1287
|
+
ctx.putImageData(imgData, 0, 0);
|
|
1288
|
+
return outCanvas;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/services/scanner.service.ts
|
|
1292
|
+
var ScannerService = class {
|
|
1293
|
+
/**
|
|
1294
|
+
* Process a new raw image file or Data URL into a fully-initialized ScanPage.
|
|
1295
|
+
*
|
|
1296
|
+
* @param imageSource File object or Image Data URL
|
|
1297
|
+
* @returns Promise resolving to a fresh ScanPage object
|
|
1298
|
+
*/
|
|
1299
|
+
async createScanPage(imageSource) {
|
|
1300
|
+
const dataUrl = typeof imageSource === "string" ? imageSource : await this.fileToDataUrl(imageSource);
|
|
1301
|
+
const img = await this.loadImage(dataUrl);
|
|
1302
|
+
const width = img.naturalWidth || img.width;
|
|
1303
|
+
const height = img.naturalHeight || img.height;
|
|
1304
|
+
const cv = opencvService.isReady() ? window.cv : void 0;
|
|
1305
|
+
const cropQuad = detectDocumentEdges(img, cv);
|
|
1306
|
+
const pageId = `page_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
1307
|
+
const baseCanvas = this.imageToCanvas(img);
|
|
1308
|
+
const unwarpedCanvas = applyPerspectiveTransform(baseCanvas, cropQuad, cv);
|
|
1309
|
+
const enhancedCanvas = applyEnhancementFilter(unwarpedCanvas, "enhanced", 0, 0, cv);
|
|
1310
|
+
const processedUrl = enhancedCanvas.toDataURL("image/jpeg", 0.88);
|
|
1311
|
+
return {
|
|
1312
|
+
id: pageId,
|
|
1313
|
+
originalUrl: dataUrl,
|
|
1314
|
+
processedUrl,
|
|
1315
|
+
cropQuad,
|
|
1316
|
+
filter: "enhanced",
|
|
1317
|
+
rotation: 0,
|
|
1318
|
+
brightness: 0,
|
|
1319
|
+
contrast: 0,
|
|
1320
|
+
width,
|
|
1321
|
+
height,
|
|
1322
|
+
timestamp: Date.now()
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Re-process an existing ScanPage when user modifies cropQuad, filter, rotation, brightness, or contrast.
|
|
1327
|
+
*
|
|
1328
|
+
* @param page ScanPage to update
|
|
1329
|
+
* @returns Promise resolving to updated ScanPage with fresh processedUrl
|
|
1330
|
+
*/
|
|
1331
|
+
async reprocessScanPage(page) {
|
|
1332
|
+
const img = await this.loadImage(page.originalUrl);
|
|
1333
|
+
const cv = opencvService.isReady() ? window.cv : void 0;
|
|
1334
|
+
const baseCanvas = this.imageToCanvas(img);
|
|
1335
|
+
const unwarpedCanvas = applyPerspectiveTransform(baseCanvas, page.cropQuad, cv);
|
|
1336
|
+
const enhancedCanvas = applyEnhancementFilter(
|
|
1337
|
+
unwarpedCanvas,
|
|
1338
|
+
page.filter,
|
|
1339
|
+
page.brightness,
|
|
1340
|
+
page.contrast,
|
|
1341
|
+
cv
|
|
1342
|
+
);
|
|
1343
|
+
const processedUrl = enhancedCanvas.toDataURL("image/jpeg", 0.88);
|
|
1344
|
+
return {
|
|
1345
|
+
...page,
|
|
1346
|
+
processedUrl
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Helper to convert File to Data URL string.
|
|
1351
|
+
*/
|
|
1352
|
+
fileToDataUrl(file) {
|
|
1353
|
+
return new Promise((resolve, reject) => {
|
|
1354
|
+
const reader = new FileReader();
|
|
1355
|
+
reader.onload = () => resolve(reader.result);
|
|
1356
|
+
reader.onerror = (err) => reject(err);
|
|
1357
|
+
reader.readAsDataURL(file);
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Helper to load Image element asynchronously.
|
|
1362
|
+
*/
|
|
1363
|
+
loadImage(src) {
|
|
1364
|
+
return new Promise((resolve, reject) => {
|
|
1365
|
+
const img = new Image();
|
|
1366
|
+
img.crossOrigin = "anonymous";
|
|
1367
|
+
img.onload = () => resolve(img);
|
|
1368
|
+
img.onerror = (err) => reject(err);
|
|
1369
|
+
img.src = src;
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
imageToCanvas(img) {
|
|
1373
|
+
const canvas = document.createElement("canvas");
|
|
1374
|
+
const maxDim = 2048;
|
|
1375
|
+
let w = img.naturalWidth || img.width;
|
|
1376
|
+
let h = img.naturalHeight || img.height;
|
|
1377
|
+
if (w > maxDim || h > maxDim) {
|
|
1378
|
+
if (w > h) {
|
|
1379
|
+
h = Math.round(h * maxDim / w);
|
|
1380
|
+
w = maxDim;
|
|
1381
|
+
} else {
|
|
1382
|
+
w = Math.round(w * maxDim / h);
|
|
1383
|
+
h = maxDim;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
canvas.width = w;
|
|
1387
|
+
canvas.height = h;
|
|
1388
|
+
const ctx = canvas.getContext("2d");
|
|
1389
|
+
if (ctx) {
|
|
1390
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
1391
|
+
}
|
|
1392
|
+
return canvas;
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
var scannerService = new ScannerService();
|
|
1396
|
+
|
|
1397
|
+
// src/services/share.service.ts
|
|
1398
|
+
import { toast } from "sonner";
|
|
1399
|
+
function generateSecureFileUrl(fileId) {
|
|
1400
|
+
if (typeof window === "undefined") {
|
|
1401
|
+
return `/files/document/${fileId}`;
|
|
1402
|
+
}
|
|
1403
|
+
return `${window.location.origin}/files/document/${fileId}`;
|
|
1404
|
+
}
|
|
1405
|
+
async function shareFileLink(fileId, fileName) {
|
|
1406
|
+
const shareUrl = generateSecureFileUrl(fileId);
|
|
1407
|
+
if (navigator.share) {
|
|
1408
|
+
try {
|
|
1409
|
+
await navigator.share({
|
|
1410
|
+
title: `Share: ${fileName}`,
|
|
1411
|
+
text: `Check out this file: ${fileName}`,
|
|
1412
|
+
url: shareUrl
|
|
1413
|
+
});
|
|
1414
|
+
toast.success("Share link sent.");
|
|
1415
|
+
return;
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
if (err.name === "AbortError") {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
try {
|
|
1423
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
1424
|
+
toast.success("Share link copied.");
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
const textarea = document.createElement("textarea");
|
|
1427
|
+
textarea.value = shareUrl;
|
|
1428
|
+
textarea.style.position = "fixed";
|
|
1429
|
+
textarea.style.opacity = "0";
|
|
1430
|
+
document.body.appendChild(textarea);
|
|
1431
|
+
textarea.select();
|
|
1432
|
+
try {
|
|
1433
|
+
document.execCommand("copy");
|
|
1434
|
+
toast.success("Share link copied.");
|
|
1435
|
+
} catch {
|
|
1436
|
+
toast.error("Failed to copy share link.");
|
|
1437
|
+
}
|
|
1438
|
+
document.body.removeChild(textarea);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/services/upload.service.ts
|
|
1443
|
+
var UploadService = class {
|
|
1444
|
+
/**
|
|
1445
|
+
* Upload scanned PDF file to Supabase Storage via `/api/upload` route.
|
|
1446
|
+
*
|
|
1447
|
+
* @param pdfFile Compiled PDF File object
|
|
1448
|
+
* @param pageCount Total number of pages included in document
|
|
1449
|
+
* @returns ScannedPdfResult containing publicUrl, fileName, fileSize, mimeType, and storagePath
|
|
1450
|
+
*/
|
|
1451
|
+
async uploadScannedPdf(pdfFile, pageCount) {
|
|
1452
|
+
const formData = new FormData();
|
|
1453
|
+
formData.append("file", pdfFile);
|
|
1454
|
+
formData.append("folder", "documents");
|
|
1455
|
+
const response = await fetch("/api/upload", {
|
|
1456
|
+
method: "POST",
|
|
1457
|
+
body: formData
|
|
1458
|
+
});
|
|
1459
|
+
if (!response.ok) {
|
|
1460
|
+
const errText = await response.text();
|
|
1461
|
+
throw new Error(`Upload failed (${response.status}): ${errText}`);
|
|
1462
|
+
}
|
|
1463
|
+
const data = await response.json();
|
|
1464
|
+
if (!data.success || !data.publicUrl) {
|
|
1465
|
+
throw new Error(data.error || "Server did not return a valid public upload URL");
|
|
1466
|
+
}
|
|
1467
|
+
return {
|
|
1468
|
+
publicUrl: data.publicUrl,
|
|
1469
|
+
fileName: data.fileName || pdfFile.name,
|
|
1470
|
+
fileSize: data.fileSize || pdfFile.size,
|
|
1471
|
+
mimeType: "application/pdf",
|
|
1472
|
+
storagePath: data.storagePath || `scanned/${pdfFile.name}`,
|
|
1473
|
+
pageCount
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
var scannerUploadService = new UploadService();
|
|
1478
|
+
|
|
1479
|
+
// src/services/auth-redirect.service.ts
|
|
1480
|
+
function getFullRedirectUrl(pathname, searchParamsString) {
|
|
1481
|
+
if (!searchParamsString) {
|
|
1482
|
+
return pathname;
|
|
1483
|
+
}
|
|
1484
|
+
const cleanSearch = searchParamsString.startsWith("?") ? searchParamsString : `?${searchParamsString}`;
|
|
1485
|
+
return `${pathname}${cleanSearch}`;
|
|
1486
|
+
}
|
|
1487
|
+
function handleAuthRedirect(router, redirectTo) {
|
|
1488
|
+
console.log("[DEBUG client] handleAuthRedirect received raw redirectTo:", redirectTo);
|
|
1489
|
+
let targetPath = "/";
|
|
1490
|
+
if (redirectTo) {
|
|
1491
|
+
try {
|
|
1492
|
+
targetPath = decodeURIComponent(redirectTo);
|
|
1493
|
+
} catch {
|
|
1494
|
+
targetPath = redirectTo;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
try {
|
|
1498
|
+
if (targetPath.startsWith("http://") || targetPath.startsWith("https://")) {
|
|
1499
|
+
const url = new URL(targetPath);
|
|
1500
|
+
targetPath = `${url.pathname}${url.search}${url.hash}` || "/";
|
|
1501
|
+
}
|
|
1502
|
+
} catch {
|
|
1503
|
+
}
|
|
1504
|
+
if (targetPath === "/sign-in" || targetPath === "/sign-up" || !targetPath || targetPath === "") {
|
|
1505
|
+
targetPath = "/";
|
|
1506
|
+
}
|
|
1507
|
+
if (!targetPath.startsWith("/")) {
|
|
1508
|
+
targetPath = `/${targetPath}`;
|
|
1509
|
+
}
|
|
1510
|
+
console.log("[DEBUG client] handleAuthRedirect executing navigation to targetPath:", targetPath);
|
|
1511
|
+
if (router && typeof router.replace === "function") {
|
|
1512
|
+
router.replace(targetPath);
|
|
1513
|
+
}
|
|
1514
|
+
if (typeof window !== "undefined") {
|
|
1515
|
+
setTimeout(() => {
|
|
1516
|
+
if (window.location.pathname.startsWith("/sign-in") || window.location.pathname.startsWith("/sign-up")) {
|
|
1517
|
+
console.log("[DEBUG client] Executing hard redirect to dashboard:", targetPath);
|
|
1518
|
+
window.location.href = targetPath;
|
|
1519
|
+
}
|
|
1520
|
+
}, 100);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/services/profile.service.ts
|
|
1525
|
+
async function ensureProfileExists(user) {
|
|
1526
|
+
try {
|
|
1527
|
+
const supabase2 = createClient();
|
|
1528
|
+
const fallbackUuid = stringToUuid(user.accountNo || user.email);
|
|
1529
|
+
const { data: existingProfile } = await supabase2.from("profiles").select("id, auth_user_id").eq("email", user.email.toLowerCase()).maybeSingle();
|
|
1530
|
+
const profileId = existingProfile?.id || fallbackUuid;
|
|
1531
|
+
const profileData = {
|
|
1532
|
+
id: profileId,
|
|
1533
|
+
name: user.name || user.email.split("@")[0],
|
|
1534
|
+
email: user.email.toLowerCase(),
|
|
1535
|
+
avatar: user.picture || null,
|
|
1536
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1537
|
+
};
|
|
1538
|
+
if (user.mobile) {
|
|
1539
|
+
profileData.mobile = user.mobile;
|
|
1540
|
+
}
|
|
1541
|
+
profileData.auth_user_id = existingProfile?.auth_user_id || profileId;
|
|
1542
|
+
if (existingProfile) {
|
|
1543
|
+
await supabase2.from("profiles").update(profileData).eq("id", existingProfile.id);
|
|
1544
|
+
} else {
|
|
1545
|
+
await supabase2.from("profiles").insert(profileData);
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
id: profileId,
|
|
1549
|
+
name: profileData.name,
|
|
1550
|
+
email: profileData.email,
|
|
1551
|
+
avatar_url: profileData.avatar
|
|
1552
|
+
};
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
console.error("[ProfileService] Error ensuring profile exists:", err);
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
async function updateProfile(id, updates) {
|
|
1559
|
+
try {
|
|
1560
|
+
const supabase2 = createClient();
|
|
1561
|
+
await supabase2.from("profiles").update(updates).eq("id", id);
|
|
1562
|
+
return true;
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
console.error("[ProfileService] Error updating profile:", err);
|
|
1565
|
+
return false;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async function getProfileByEmail(email) {
|
|
1569
|
+
try {
|
|
1570
|
+
const supabase2 = createClient();
|
|
1571
|
+
const { data } = await supabase2.from("profiles").select("*").eq("email", email).maybeSingle();
|
|
1572
|
+
if (!data) return null;
|
|
1573
|
+
return {
|
|
1574
|
+
id: data.id,
|
|
1575
|
+
name: data.name,
|
|
1576
|
+
email: data.email,
|
|
1577
|
+
avatar_url: data.avatar
|
|
1578
|
+
};
|
|
1579
|
+
} catch (err) {
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/services/push-notification.service.ts
|
|
1585
|
+
async function requestNativeAppPermissions() {
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
async function initPushNotifications(userId) {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/lib/short-url-client.ts
|
|
1593
|
+
var ID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
1594
|
+
function generateShortId(length = 6) {
|
|
1595
|
+
let result = "";
|
|
1596
|
+
for (let i = 0; i < length; i++) {
|
|
1597
|
+
result += ID_CHARS.charAt(Math.floor(Math.random() * ID_CHARS.length));
|
|
1598
|
+
}
|
|
1599
|
+
return result;
|
|
1600
|
+
}
|
|
1601
|
+
function toBase64Url(str) {
|
|
1602
|
+
const bytes = new TextEncoder().encode(str);
|
|
1603
|
+
const bin = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
1604
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1605
|
+
}
|
|
1606
|
+
function buildSelfContainedShortUrl(origin, configHash, durationHours) {
|
|
1607
|
+
const expiresAtMs = Date.now() + durationHours * 60 * 60 * 1e3;
|
|
1608
|
+
const targetUrl = `${origin}/l?c=${configHash}&exp=${expiresAtMs}`;
|
|
1609
|
+
const id = generateShortId();
|
|
1610
|
+
const encoded = toBase64Url(targetUrl);
|
|
1611
|
+
return {
|
|
1612
|
+
shortUrl: `${origin}/go/${id}?r=${encoded}`,
|
|
1613
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/lib/short-url-store.ts
|
|
1618
|
+
import { promises as fs } from "fs";
|
|
1619
|
+
import path from "path";
|
|
1620
|
+
import { createClient as createClient2 } from "@supabase/supabase-js";
|
|
1621
|
+
var supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
1622
|
+
var supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
|
|
1623
|
+
var supabase = supabaseUrl && supabaseAnonKey ? createClient2(supabaseUrl, supabaseAnonKey) : null;
|
|
1624
|
+
var URLS_FILE = path.join(process.cwd(), "src/features/link-builder/data/urls.json");
|
|
1625
|
+
var ID_CHARS2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
1626
|
+
function getMemoryStore() {
|
|
1627
|
+
const g = globalThis;
|
|
1628
|
+
if (!g.__shortUrlStore) {
|
|
1629
|
+
g.__shortUrlStore = /* @__PURE__ */ new Map();
|
|
1630
|
+
}
|
|
1631
|
+
return g.__shortUrlStore;
|
|
1632
|
+
}
|
|
1633
|
+
function generateShortId2(length = 6) {
|
|
1634
|
+
let result = "";
|
|
1635
|
+
for (let i = 0; i < length; i++) {
|
|
1636
|
+
result += ID_CHARS2.charAt(Math.floor(Math.random() * ID_CHARS2.length));
|
|
1637
|
+
}
|
|
1638
|
+
return result;
|
|
1639
|
+
}
|
|
1640
|
+
function isKvConfigured() {
|
|
1641
|
+
return Boolean(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN);
|
|
1642
|
+
}
|
|
1643
|
+
async function kvGet(key) {
|
|
1644
|
+
if (!isKvConfigured()) return null;
|
|
1645
|
+
try {
|
|
1646
|
+
const res = await fetch(process.env.KV_REST_API_URL, {
|
|
1647
|
+
method: "POST",
|
|
1648
|
+
headers: {
|
|
1649
|
+
Authorization: `Bearer ${process.env.KV_REST_API_TOKEN}`,
|
|
1650
|
+
"Content-Type": "application/json"
|
|
1651
|
+
},
|
|
1652
|
+
body: JSON.stringify(["GET", key]),
|
|
1653
|
+
cache: "no-store"
|
|
1654
|
+
});
|
|
1655
|
+
if (!res.ok) return null;
|
|
1656
|
+
const data = await res.json();
|
|
1657
|
+
if (!data.result) return null;
|
|
1658
|
+
return JSON.parse(data.result);
|
|
1659
|
+
} catch {
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
async function kvSet(key, entry, ttlSeconds) {
|
|
1664
|
+
if (!isKvConfigured()) return false;
|
|
1665
|
+
try {
|
|
1666
|
+
const res = await fetch(process.env.KV_REST_API_URL, {
|
|
1667
|
+
method: "POST",
|
|
1668
|
+
headers: {
|
|
1669
|
+
Authorization: `Bearer ${process.env.KV_REST_API_TOKEN}`,
|
|
1670
|
+
"Content-Type": "application/json"
|
|
1671
|
+
},
|
|
1672
|
+
body: JSON.stringify(["SET", key, JSON.stringify(entry), "EX", ttlSeconds]),
|
|
1673
|
+
cache: "no-store"
|
|
1674
|
+
});
|
|
1675
|
+
return res.ok;
|
|
1676
|
+
} catch {
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
async function readFileStore() {
|
|
1681
|
+
try {
|
|
1682
|
+
const raw = await fs.readFile(URLS_FILE, "utf-8");
|
|
1683
|
+
return JSON.parse(raw);
|
|
1684
|
+
} catch {
|
|
1685
|
+
return [];
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
async function writeFileStore(entries) {
|
|
1689
|
+
try {
|
|
1690
|
+
await fs.writeFile(URLS_FILE, JSON.stringify(entries, null, 2), "utf-8");
|
|
1691
|
+
return true;
|
|
1692
|
+
} catch {
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
async function saveShortUrl(targetUrl, expiresAtMs) {
|
|
1697
|
+
const id = generateShortId2();
|
|
1698
|
+
const entry = {
|
|
1699
|
+
id,
|
|
1700
|
+
targetUrl,
|
|
1701
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1702
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
1703
|
+
};
|
|
1704
|
+
const memory = getMemoryStore();
|
|
1705
|
+
memory.set(id, entry);
|
|
1706
|
+
const ttlSeconds = Math.max(60, Math.ceil((expiresAtMs - Date.now()) / 1e3));
|
|
1707
|
+
const kvSaved = await kvSet(`short:${id}`, entry, ttlSeconds);
|
|
1708
|
+
const existing = await readFileStore();
|
|
1709
|
+
existing.push(entry);
|
|
1710
|
+
const fileSaved = await writeFileStore(existing);
|
|
1711
|
+
let supabaseSaved = false;
|
|
1712
|
+
if (supabase) {
|
|
1713
|
+
try {
|
|
1714
|
+
const { error } = await supabase.from("short_urls").insert({
|
|
1715
|
+
id,
|
|
1716
|
+
target_url: targetUrl,
|
|
1717
|
+
expires_at: new Date(expiresAtMs).toISOString()
|
|
1718
|
+
});
|
|
1719
|
+
if (error) {
|
|
1720
|
+
console.error("Supabase saveShortUrl error:", error);
|
|
1721
|
+
} else {
|
|
1722
|
+
supabaseSaved = true;
|
|
1723
|
+
console.log("Supabase saveShortUrl success:", id);
|
|
1724
|
+
}
|
|
1725
|
+
} catch (e) {
|
|
1726
|
+
console.error("Supabase saveShortUrl exception:", e);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
const needsFallback = !kvSaved && !fileSaved && !supabaseSaved;
|
|
1730
|
+
const shortUrlSuffix = needsFallback ? `${id}?r=${Buffer.from(targetUrl).toString("base64url")}` : id;
|
|
1731
|
+
return { id, entry, shortUrlSuffix };
|
|
1732
|
+
}
|
|
1733
|
+
var fileStoreLoaded = false;
|
|
1734
|
+
async function ensureFileStoreLoaded() {
|
|
1735
|
+
if (fileStoreLoaded) return;
|
|
1736
|
+
fileStoreLoaded = true;
|
|
1737
|
+
const entries = await readFileStore();
|
|
1738
|
+
const memory = getMemoryStore();
|
|
1739
|
+
for (const entry of entries) {
|
|
1740
|
+
memory.set(entry.id, entry);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async function getShortUrl(id) {
|
|
1744
|
+
await ensureFileStoreLoaded();
|
|
1745
|
+
const memory = getMemoryStore();
|
|
1746
|
+
const cached = memory.get(id);
|
|
1747
|
+
if (cached) return cached;
|
|
1748
|
+
if (supabase) {
|
|
1749
|
+
try {
|
|
1750
|
+
const { data, error } = await supabase.from("short_urls").select("*").eq("id", id).single();
|
|
1751
|
+
if (!error && data) {
|
|
1752
|
+
const entry2 = {
|
|
1753
|
+
id: data.id,
|
|
1754
|
+
targetUrl: data.target_url,
|
|
1755
|
+
createdAt: data.created_at,
|
|
1756
|
+
expiresAt: data.expires_at
|
|
1757
|
+
};
|
|
1758
|
+
memory.set(id, entry2);
|
|
1759
|
+
return entry2;
|
|
1760
|
+
}
|
|
1761
|
+
} catch (e) {
|
|
1762
|
+
console.error("Supabase getShortUrl exception:", e);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
const fromKv = await kvGet(`short:${id}`);
|
|
1766
|
+
if (fromKv) {
|
|
1767
|
+
memory.set(id, fromKv);
|
|
1768
|
+
return fromKv;
|
|
1769
|
+
}
|
|
1770
|
+
const fromFile = await readFileStore();
|
|
1771
|
+
const entry = fromFile.find((e) => e.id === id) ?? null;
|
|
1772
|
+
if (entry) {
|
|
1773
|
+
memory.set(id, entry);
|
|
1774
|
+
}
|
|
1775
|
+
return entry;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// src/services/short-url.service.ts
|
|
1779
|
+
var ShortUrlService = class {
|
|
1780
|
+
/**
|
|
1781
|
+
* Generates a random short ID of specified length.
|
|
1782
|
+
*/
|
|
1783
|
+
static generateId(length = 6) {
|
|
1784
|
+
return generateShortId(length);
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Encodes a string to a base64url format.
|
|
1788
|
+
*/
|
|
1789
|
+
static encodeUrl(url) {
|
|
1790
|
+
return toBase64Url(url);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Builds a self-contained client-side short URL with embedded expiration.
|
|
1794
|
+
*/
|
|
1795
|
+
static buildSelfContainedUrl(origin, configHash, durationHours) {
|
|
1796
|
+
return buildSelfContainedShortUrl(origin, configHash, durationHours);
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Stores a shortened URL mapping with expiration timestamp.
|
|
1800
|
+
*/
|
|
1801
|
+
static async saveUrl(targetUrl, expiresAtMs) {
|
|
1802
|
+
return saveShortUrl(targetUrl, expiresAtMs);
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Retrieves a shortened URL target mapping.
|
|
1806
|
+
*/
|
|
1807
|
+
static async getUrl(shortUrlSuffix) {
|
|
1808
|
+
return getShortUrl(shortUrlSuffix);
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1812
|
+
// src/services/ai-search.service.ts
|
|
1813
|
+
import axios from "axios";
|
|
1814
|
+
var AiSearchService = class {
|
|
1815
|
+
/**
|
|
1816
|
+
* Executes web search through Tavily API.
|
|
1817
|
+
*/
|
|
1818
|
+
static async searchWeb(query, apiKey) {
|
|
1819
|
+
if (!apiKey) {
|
|
1820
|
+
throw new Error("Tavily API key is not configured.");
|
|
1821
|
+
}
|
|
1822
|
+
const res = await axios.post(
|
|
1823
|
+
"https://api.tavily.com/search",
|
|
1824
|
+
{
|
|
1825
|
+
api_key: apiKey,
|
|
1826
|
+
query,
|
|
1827
|
+
search_depth: "advanced",
|
|
1828
|
+
max_results: 10,
|
|
1829
|
+
include_images: true
|
|
1830
|
+
},
|
|
1831
|
+
{ timeout: 15e3 }
|
|
1832
|
+
);
|
|
1833
|
+
return {
|
|
1834
|
+
results: res.data?.results || [],
|
|
1835
|
+
images: res.data?.images || []
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Generates answer analysis using Gemini API.
|
|
1840
|
+
*/
|
|
1841
|
+
static async generateAnswer(prompt, apiKey) {
|
|
1842
|
+
if (!apiKey) {
|
|
1843
|
+
throw new Error("Gemini API key is not configured.");
|
|
1844
|
+
}
|
|
1845
|
+
const res = await axios.post(
|
|
1846
|
+
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`,
|
|
1847
|
+
{
|
|
1848
|
+
contents: [
|
|
1849
|
+
{
|
|
1850
|
+
parts: [{ text: prompt }]
|
|
1851
|
+
}
|
|
1852
|
+
]
|
|
1853
|
+
},
|
|
1854
|
+
{ timeout: 3e4 }
|
|
1855
|
+
);
|
|
1856
|
+
return res.data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
// src/features/vouchers/repositories/voucher-repository.ts
|
|
1861
|
+
async function getActiveUserId() {
|
|
1862
|
+
try {
|
|
1863
|
+
const supabase2 = createClient();
|
|
1864
|
+
const { data: user } = await supabase2.auth.getUser();
|
|
1865
|
+
if (user?.user?.id) return user.user.id;
|
|
1866
|
+
} catch {
|
|
1867
|
+
}
|
|
1868
|
+
const storeUser = useAuthStore.getState().auth.user;
|
|
1869
|
+
if (!storeUser?.email && !storeUser?.id) return null;
|
|
1870
|
+
try {
|
|
1871
|
+
const supabase2 = createClient();
|
|
1872
|
+
const email = storeUser.email?.toLowerCase();
|
|
1873
|
+
if (email) {
|
|
1874
|
+
const { data: profileRow } = await supabase2.from("profiles").select("id").eq("email", email).maybeSingle();
|
|
1875
|
+
if (profileRow?.id) return profileRow.id;
|
|
1876
|
+
}
|
|
1877
|
+
} catch {
|
|
1878
|
+
}
|
|
1879
|
+
if (storeUser?.id) return stringToUuid(storeUser.id);
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
async function uploadVoucherFile(file, subfolder = "originals") {
|
|
1883
|
+
const supabase2 = createClient();
|
|
1884
|
+
const userId = await getActiveUserId() || "anonymous";
|
|
1885
|
+
const fileExt = file.name.split(".").pop() || "bin";
|
|
1886
|
+
const uniqueName = `${crypto.randomUUID()}.${fileExt}`;
|
|
1887
|
+
const path2 = `vouchers/${userId}/${subfolder}/${uniqueName}`;
|
|
1888
|
+
const { error } = await supabase2.storage.from("chat-files").upload(path2, file, { upsert: false, contentType: file.type || "application/octet-stream" });
|
|
1889
|
+
if (error) {
|
|
1890
|
+
throw new Error(`Storage upload failed: ${error.message}`);
|
|
1891
|
+
}
|
|
1892
|
+
const { data: urlData } = supabase2.storage.from("chat-files").getPublicUrl(path2);
|
|
1893
|
+
return urlData.publicUrl;
|
|
1894
|
+
}
|
|
1895
|
+
async function uploadVoucherBlob(blob, fileName) {
|
|
1896
|
+
const supabase2 = createClient();
|
|
1897
|
+
const userId = await getActiveUserId() || "anonymous";
|
|
1898
|
+
const uniqueName = `${crypto.randomUUID()}_${fileName}`;
|
|
1899
|
+
const path2 = `vouchers/${userId}/edited/${uniqueName}`;
|
|
1900
|
+
const { error } = await supabase2.storage.from("chat-files").upload(path2, blob, { upsert: false, contentType: blob.type || "application/octet-stream" });
|
|
1901
|
+
if (error) {
|
|
1902
|
+
throw new Error(`Invoice storage upload failed: ${error.message}`);
|
|
1903
|
+
}
|
|
1904
|
+
const { data: urlData } = supabase2.storage.from("chat-files").getPublicUrl(path2);
|
|
1905
|
+
return urlData.publicUrl;
|
|
1906
|
+
}
|
|
1907
|
+
async function saveVoucher(data) {
|
|
1908
|
+
const supabase2 = createClient();
|
|
1909
|
+
const userId = await getActiveUserId();
|
|
1910
|
+
if (!userId) {
|
|
1911
|
+
throw new Error("Not authenticated");
|
|
1912
|
+
}
|
|
1913
|
+
const { data: row, error } = await supabase2.from("vouchers").insert({
|
|
1914
|
+
...data,
|
|
1915
|
+
user_id: userId
|
|
1916
|
+
}).select("*").single();
|
|
1917
|
+
if (error) {
|
|
1918
|
+
throw new Error(`DB save failed: ${error.message}`);
|
|
1919
|
+
}
|
|
1920
|
+
return row;
|
|
1921
|
+
}
|
|
1922
|
+
async function getUserVouchers() {
|
|
1923
|
+
const supabase2 = createClient();
|
|
1924
|
+
const userId = await getActiveUserId();
|
|
1925
|
+
if (!userId) {
|
|
1926
|
+
const { data: data2 } = await supabase2.from("vouchers").select("*").order("created_at", { ascending: false }).limit(50);
|
|
1927
|
+
return data2 ?? [];
|
|
1928
|
+
}
|
|
1929
|
+
const { data, error } = await supabase2.from("vouchers").select("*").or(`user_id.eq.${userId}`).order("created_at", { ascending: false }).limit(50);
|
|
1930
|
+
if (error) {
|
|
1931
|
+
console.error("Failed to fetch vouchers:", error.message);
|
|
1932
|
+
const { data: fallbackData } = await supabase2.from("vouchers").select("*").order("created_at", { ascending: false }).limit(50);
|
|
1933
|
+
return fallbackData ?? [];
|
|
1934
|
+
}
|
|
1935
|
+
return data ?? [];
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// src/services/geocode.service.ts
|
|
1939
|
+
var GeocodeService = class {
|
|
1940
|
+
/**
|
|
1941
|
+
* Reverse geocodes latitude and longitude into an address.
|
|
1942
|
+
*/
|
|
1943
|
+
static async reverseGeocode(latitude, longitude) {
|
|
1944
|
+
try {
|
|
1945
|
+
const res = await fetch(`/api/geocode?lat=${latitude}&lon=${longitude}`);
|
|
1946
|
+
if (!res.ok) {
|
|
1947
|
+
throw new Error(`Geocode request failed with status ${res.status}`);
|
|
1948
|
+
}
|
|
1949
|
+
return await res.json();
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
console.error("GeocodeService reverseGeocode failed:", err);
|
|
1952
|
+
return null;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
export {
|
|
1957
|
+
AiSearchService,
|
|
1958
|
+
GeocodeService,
|
|
1959
|
+
ShortUrlService,
|
|
1960
|
+
autoSubscribeAdmin,
|
|
1961
|
+
ensureProfileExists,
|
|
1962
|
+
generateSecureFileUrl,
|
|
1963
|
+
generateSignedUrl,
|
|
1964
|
+
getFullRedirectUrl,
|
|
1965
|
+
getOrCreateAlertConversation,
|
|
1966
|
+
getProfileByEmail,
|
|
1967
|
+
getSharedFileMetadata,
|
|
1968
|
+
getUserVouchers,
|
|
1969
|
+
handleAuthRedirect,
|
|
1970
|
+
initPushNotifications,
|
|
1971
|
+
pdfService,
|
|
1972
|
+
requestNativeAppPermissions,
|
|
1973
|
+
saveVoucher,
|
|
1974
|
+
scannerService,
|
|
1975
|
+
scannerUploadService,
|
|
1976
|
+
shareFileLink,
|
|
1977
|
+
triggerContactAlert,
|
|
1978
|
+
triggerGroupAlert,
|
|
1979
|
+
updateProfile,
|
|
1980
|
+
uploadVoucherBlob,
|
|
1981
|
+
uploadVoucherFile
|
|
1982
|
+
};
|
|
1983
|
+
//# sourceMappingURL=services.mjs.map
|