Compare commits
7 Commits
16e30e1bb1
...
e2564e2a66
| Author | SHA1 | Date | |
|---|---|---|---|
| e2564e2a66 | |||
| 56c8bcfb85 | |||
| 3a2f70c831 | |||
| 3f3c9671f5 | |||
| 9dd8c32d6d | |||
| aaa2d0b48d | |||
| ea204894aa |
@@ -20,6 +20,7 @@
|
|||||||
"@workspace/api-zod": "workspace:*",
|
"@workspace/api-zod": "workspace:*",
|
||||||
"@workspace/db": "workspace:*",
|
"@workspace/db": "workspace:*",
|
||||||
"arabic-persian-reshaper": "1.0.1",
|
"arabic-persian-reshaper": "1.0.1",
|
||||||
|
"archiver": "^8.0.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"bidi-js": "^1.0.3",
|
"bidi-js": "^1.0.3",
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "^10.0.0",
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/archiver": "^8.0.0",
|
||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/connect-pg-simple": "^7.0.3",
|
"@types/connect-pg-simple": "^7.0.3",
|
||||||
"@types/cookie-parser": "^1.4.10",
|
"@types/cookie-parser": "^1.4.10",
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import {
|
|||||||
servicesTable,
|
servicesTable,
|
||||||
executiveMeetingFontSettingsTable,
|
executiveMeetingFontSettingsTable,
|
||||||
executiveMeetingPdfArchivesTable,
|
executiveMeetingPdfArchivesTable,
|
||||||
|
protocolPhotosTable,
|
||||||
rolesTable,
|
rolesTable,
|
||||||
} from "@workspace/db";
|
} from "@workspace/db";
|
||||||
import { eq, inArray, sql } from "drizzle-orm";
|
import { eq, inArray, sql } from "drizzle-orm";
|
||||||
import { getEffectiveRoleIds } from "../middlewares/auth";
|
import { getEffectiveRoleIds, userHasPermission } from "../middlewares/auth";
|
||||||
import { getVisibleAppsForUser } from "./appsVisibility";
|
import { getVisibleAppsForUser } from "./appsVisibility";
|
||||||
|
|
||||||
// Roles that gate read access to the Executive Meetings module.
|
// Roles that gate read access to the Executive Meetings module.
|
||||||
@@ -152,6 +153,18 @@ export async function canUserReadObjectPath(
|
|||||||
const rows = (meeting as unknown as { rows?: unknown[] }).rows ?? [];
|
const rows = (meeting as unknown as { rows?: unknown[] }).rows ?? [];
|
||||||
if (rows.length > 0) return hasExecutiveAccess;
|
if (rows.length > 0) return hasExecutiveAccess;
|
||||||
|
|
||||||
|
// 7. Protocol visit photos — gated by the protocol read permission
|
||||||
|
// (`protocol.access`), the same scoped permission that makes the
|
||||||
|
// Protocol tile visible. Never gated by role name (module convention).
|
||||||
|
const photo = await db
|
||||||
|
.select({ id: protocolPhotosTable.id })
|
||||||
|
.from(protocolPhotosTable)
|
||||||
|
.where(eq(protocolPhotosTable.objectPath, objectPath))
|
||||||
|
.limit(1);
|
||||||
|
if (photo.length > 0) {
|
||||||
|
return isAdmin || (await userHasPermission(userId, "protocol.access"));
|
||||||
|
}
|
||||||
|
|
||||||
// Orphan path — no entity references it. Treat as not-found for
|
// Orphan path — no entity references it. Treat as not-found for
|
||||||
// every role (including admin). This is what stops object
|
// every role (including admin). This is what stops object
|
||||||
// enumeration: even with admin credentials, you cannot probe
|
// enumeration: even with admin credentials, you cannot probe
|
||||||
|
|||||||
@@ -25,6 +25,30 @@ export class ObjectNotFoundError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map a stored object's content type to a sensible file extension so
|
||||||
|
// downloads land with a usable name (stored paths are extension-less UUIDs).
|
||||||
|
const CONTENT_TYPE_EXTENSIONS: Record<string, string> = {
|
||||||
|
"image/jpeg": "jpg",
|
||||||
|
"image/jpg": "jpg",
|
||||||
|
"image/png": "png",
|
||||||
|
"image/gif": "gif",
|
||||||
|
"image/webp": "webp",
|
||||||
|
"image/heic": "heic",
|
||||||
|
"image/heif": "heif",
|
||||||
|
"image/bmp": "bmp",
|
||||||
|
"image/tiff": "tiff",
|
||||||
|
"image/svg+xml": "svg",
|
||||||
|
"application/pdf": "pdf",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function extensionForContentType(
|
||||||
|
contentType: string | undefined,
|
||||||
|
): string {
|
||||||
|
if (!contentType) return "bin";
|
||||||
|
const base = contentType.split(";")[0].trim().toLowerCase();
|
||||||
|
return CONTENT_TYPE_EXTENSIONS[base] ?? "bin";
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Driver interface
|
// Driver interface
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
protocolExternalMeetingsTable,
|
protocolExternalMeetingsTable,
|
||||||
protocolGiftsTable,
|
protocolGiftsTable,
|
||||||
protocolGiftIssuesTable,
|
protocolGiftIssuesTable,
|
||||||
|
protocolPhotoAlbumsTable,
|
||||||
|
protocolPhotosTable,
|
||||||
protocolAuditLogsTable,
|
protocolAuditLogsTable,
|
||||||
rolesTable,
|
rolesTable,
|
||||||
permissionsTable,
|
permissionsTable,
|
||||||
@@ -21,9 +23,16 @@ import {
|
|||||||
getEffectiveRoleIds,
|
getEffectiveRoleIds,
|
||||||
userHasPermission,
|
userHasPermission,
|
||||||
} from "../middlewares/auth";
|
} from "../middlewares/auth";
|
||||||
|
import {
|
||||||
|
ObjectStorageService,
|
||||||
|
extensionForContentType,
|
||||||
|
} from "../lib/objectStorage";
|
||||||
|
import { ZipArchive } from "archiver";
|
||||||
|
import { Readable } from "stream";
|
||||||
import type { Request, Response, NextFunction } from "express";
|
import type { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
const objectStorage = new ObjectStorageService();
|
||||||
|
|
||||||
router.param("id", (req, res, next, value) => {
|
router.param("id", (req, res, next, value) => {
|
||||||
if (!/^\d+$/.test(String(value))) {
|
if (!/^\d+$/.test(String(value))) {
|
||||||
@@ -1848,4 +1857,377 @@ router.get(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Visit photo albums (تصوير) — album + photo CRUD.
|
||||||
|
//
|
||||||
|
// Reads are gated by requireProtocolAccess; every mutation by requireMutate
|
||||||
|
// (both also enforce the user is active). Photos reference private object-
|
||||||
|
// storage paths served through GET /api/storage/objects/*, authorized per
|
||||||
|
// entity in lib/objectAuthz.ts.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const albumCreateSchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(300),
|
||||||
|
externalMeetingId: z.number().int().positive().nullable().optional(),
|
||||||
|
});
|
||||||
|
const albumPatchSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().trim().min(1).max(300).optional(),
|
||||||
|
externalMeetingId: z.number().int().positive().nullable().optional(),
|
||||||
|
})
|
||||||
|
.refine((v) => Object.keys(v).length > 0, {
|
||||||
|
message: "no fields to update",
|
||||||
|
});
|
||||||
|
const photoCreateSchema = z.object({
|
||||||
|
objectPath: z.string().trim().min(1).max(500),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function externalMeetingExists(id: number): Promise<boolean> {
|
||||||
|
const [m] = await db
|
||||||
|
.select({ id: protocolExternalMeetingsTable.id })
|
||||||
|
.from(protocolExternalMeetingsTable)
|
||||||
|
.where(eq(protocolExternalMeetingsTable.id, id));
|
||||||
|
return !!m;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/protocol/photo-albums",
|
||||||
|
requireProtocolAccess,
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const meetingIdRaw = req.query.meetingId;
|
||||||
|
const meetingId =
|
||||||
|
typeof meetingIdRaw === "string" && /^\d+$/.test(meetingIdRaw)
|
||||||
|
? Number(meetingIdRaw)
|
||||||
|
: undefined;
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
id: protocolPhotoAlbumsTable.id,
|
||||||
|
name: protocolPhotoAlbumsTable.name,
|
||||||
|
externalMeetingId: protocolPhotoAlbumsTable.externalMeetingId,
|
||||||
|
meetingTitle: protocolExternalMeetingsTable.title,
|
||||||
|
createdBy: protocolPhotoAlbumsTable.createdBy,
|
||||||
|
createdAt: protocolPhotoAlbumsTable.createdAt,
|
||||||
|
updatedAt: protocolPhotoAlbumsTable.updatedAt,
|
||||||
|
photoCount: sql<number>`count(${protocolPhotosTable.id})::int`,
|
||||||
|
coverPath: sql<
|
||||||
|
string | null
|
||||||
|
>`(array_agg(${protocolPhotosTable.objectPath} order by ${protocolPhotosTable.sortOrder} asc, ${protocolPhotosTable.id} asc) filter (where ${protocolPhotosTable.id} is not null))[1]`,
|
||||||
|
})
|
||||||
|
.from(protocolPhotoAlbumsTable)
|
||||||
|
.leftJoin(
|
||||||
|
protocolExternalMeetingsTable,
|
||||||
|
eq(
|
||||||
|
protocolExternalMeetingsTable.id,
|
||||||
|
protocolPhotoAlbumsTable.externalMeetingId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
protocolPhotosTable,
|
||||||
|
eq(protocolPhotosTable.albumId, protocolPhotoAlbumsTable.id),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
meetingId !== undefined
|
||||||
|
? eq(protocolPhotoAlbumsTable.externalMeetingId, meetingId)
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
.groupBy(
|
||||||
|
protocolPhotoAlbumsTable.id,
|
||||||
|
protocolExternalMeetingsTable.title,
|
||||||
|
)
|
||||||
|
.orderBy(desc(protocolPhotoAlbumsTable.createdAt));
|
||||||
|
res.json(rows);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/protocol/photo-albums",
|
||||||
|
requireMutate,
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const body = parseBody(res, albumCreateSchema, req.body);
|
||||||
|
if (!body) return;
|
||||||
|
if (
|
||||||
|
body.externalMeetingId != null &&
|
||||||
|
!(await externalMeetingExists(body.externalMeetingId))
|
||||||
|
) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "external_meeting not found", code: "validation" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [row] = await db
|
||||||
|
.insert(protocolPhotoAlbumsTable)
|
||||||
|
.values({
|
||||||
|
name: body.name,
|
||||||
|
externalMeetingId: body.externalMeetingId ?? null,
|
||||||
|
createdBy: req.session.userId!,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
await logAudit(db, {
|
||||||
|
action: "photo_album.create",
|
||||||
|
entityType: "photo_album",
|
||||||
|
entityId: row.id,
|
||||||
|
newValue: row,
|
||||||
|
performedBy: req.session.userId!,
|
||||||
|
});
|
||||||
|
res.status(201).json(row);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/protocol/photo-albums/:id",
|
||||||
|
requireMutate,
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const body = parseBody(res, albumPatchSchema, req.body);
|
||||||
|
if (!body) return;
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(protocolPhotoAlbumsTable)
|
||||||
|
.where(eq(protocolPhotoAlbumsTable.id, id));
|
||||||
|
if (!existing) {
|
||||||
|
res.status(404).json({ error: "Not found", code: "not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
body.externalMeetingId != null &&
|
||||||
|
!(await externalMeetingExists(body.externalMeetingId))
|
||||||
|
) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "external_meeting not found", code: "validation" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [row] = await db
|
||||||
|
.update(protocolPhotoAlbumsTable)
|
||||||
|
.set({
|
||||||
|
...(body.name !== undefined ? { name: body.name } : {}),
|
||||||
|
...(body.externalMeetingId !== undefined
|
||||||
|
? { externalMeetingId: body.externalMeetingId }
|
||||||
|
: {}),
|
||||||
|
})
|
||||||
|
.where(eq(protocolPhotoAlbumsTable.id, id))
|
||||||
|
.returning();
|
||||||
|
await logAudit(db, {
|
||||||
|
action: "photo_album.update",
|
||||||
|
entityType: "photo_album",
|
||||||
|
entityId: id,
|
||||||
|
oldValue: existing,
|
||||||
|
newValue: row,
|
||||||
|
performedBy: req.session.userId!,
|
||||||
|
});
|
||||||
|
res.json(row);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/protocol/photo-albums/:id",
|
||||||
|
requireMutate,
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(protocolPhotoAlbumsTable)
|
||||||
|
.where(eq(protocolPhotoAlbumsTable.id, id));
|
||||||
|
if (!existing) {
|
||||||
|
res.status(404).json({ error: "Not found", code: "not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Photos cascade-delete via the FK; audit the album deletion.
|
||||||
|
await db
|
||||||
|
.delete(protocolPhotoAlbumsTable)
|
||||||
|
.where(eq(protocolPhotoAlbumsTable.id, id));
|
||||||
|
await logAudit(db, {
|
||||||
|
action: "photo_album.delete",
|
||||||
|
entityType: "photo_album",
|
||||||
|
entityId: id,
|
||||||
|
oldValue: existing,
|
||||||
|
performedBy: req.session.userId!,
|
||||||
|
});
|
||||||
|
res.status(204).end();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/protocol/photo-albums/:id/photos",
|
||||||
|
requireProtocolAccess,
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const albumId = Number(req.params.id);
|
||||||
|
const [album] = await db
|
||||||
|
.select({ id: protocolPhotoAlbumsTable.id })
|
||||||
|
.from(protocolPhotoAlbumsTable)
|
||||||
|
.where(eq(protocolPhotoAlbumsTable.id, albumId));
|
||||||
|
if (!album) {
|
||||||
|
res.status(404).json({ error: "Not found", code: "not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(protocolPhotosTable)
|
||||||
|
.where(eq(protocolPhotosTable.albumId, albumId))
|
||||||
|
.orderBy(asc(protocolPhotosTable.sortOrder), asc(protocolPhotosTable.id));
|
||||||
|
res.json(rows);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stream every photo of an album as a single ZIP. Guarded by protocol
|
||||||
|
// read access; each entry is named sequentially with an extension derived
|
||||||
|
// from the stored object's content type. Missing/unreadable objects are
|
||||||
|
// skipped rather than failing the whole archive.
|
||||||
|
router.get(
|
||||||
|
"/protocol/photo-albums/:id/download",
|
||||||
|
requireProtocolAccess,
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const albumId = Number(req.params.id);
|
||||||
|
const [album] = await db
|
||||||
|
.select({
|
||||||
|
id: protocolPhotoAlbumsTable.id,
|
||||||
|
name: protocolPhotoAlbumsTable.name,
|
||||||
|
})
|
||||||
|
.from(protocolPhotoAlbumsTable)
|
||||||
|
.where(eq(protocolPhotoAlbumsTable.id, albumId));
|
||||||
|
if (!album) {
|
||||||
|
res.status(404).json({ error: "Not found", code: "not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(protocolPhotosTable)
|
||||||
|
.where(eq(protocolPhotosTable.albumId, albumId))
|
||||||
|
.orderBy(asc(protocolPhotosTable.sortOrder), asc(protocolPhotosTable.id));
|
||||||
|
if (rows.length === 0) {
|
||||||
|
res.status(404).json({ error: "album is empty", code: "not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeName =
|
||||||
|
album.name
|
||||||
|
.replace(/[\r\n"\\/]/g, "")
|
||||||
|
.replace(/[\u0000-\u001f]/g, "")
|
||||||
|
.trim()
|
||||||
|
.slice(0, 180) || "album";
|
||||||
|
const asciiName = safeName.replace(/[^\x20-\x7e]/g, "_") || "album";
|
||||||
|
res.setHeader("Content-Type", "application/zip");
|
||||||
|
res.setHeader(
|
||||||
|
"Content-Disposition",
|
||||||
|
`attachment; filename="${asciiName}.zip"; filename*=UTF-8''${encodeURIComponent(
|
||||||
|
`${safeName}.zip`,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const archive = new ZipArchive({ zlib: { level: 6 } });
|
||||||
|
archive.on("error", (err: Error) => {
|
||||||
|
req.log.error({ err }, "Error building album zip");
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: "Failed to build archive" });
|
||||||
|
} else {
|
||||||
|
res.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
archive.pipe(res);
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
for (const photo of rows) {
|
||||||
|
index += 1;
|
||||||
|
try {
|
||||||
|
const file = await objectStorage.getObjectEntityFile(photo.objectPath);
|
||||||
|
const meta = await file.getMetadata();
|
||||||
|
const ext = extensionForContentType(meta.contentType);
|
||||||
|
const entryName = `${String(index).padStart(3, "0")}.${ext}`;
|
||||||
|
archive.append(file.createReadStream() as Readable, {
|
||||||
|
name: entryName,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
req.log.warn(
|
||||||
|
{ err, photoId: photo.id },
|
||||||
|
"Skipping unreadable photo in album zip",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await archive.finalize();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/protocol/photo-albums/:id/photos",
|
||||||
|
requireMutate,
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const albumId = Number(req.params.id);
|
||||||
|
const body = parseBody(res, photoCreateSchema, req.body);
|
||||||
|
if (!body) return;
|
||||||
|
const [album] = await db
|
||||||
|
.select({ id: protocolPhotoAlbumsTable.id })
|
||||||
|
.from(protocolPhotoAlbumsTable)
|
||||||
|
.where(eq(protocolPhotoAlbumsTable.id, albumId));
|
||||||
|
if (!album) {
|
||||||
|
res.status(404).json({ error: "Not found", code: "not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Normalize any signed URL / legacy form back to the canonical
|
||||||
|
// /objects/<id> path and reject anything that is not a private object.
|
||||||
|
const objectPath = objectStorage.normalizeObjectEntityPath(body.objectPath);
|
||||||
|
if (!objectPath.startsWith("/objects/")) {
|
||||||
|
res.status(400).json({ error: "invalid objectPath", code: "validation" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Verify the object was actually uploaded before storing a reference to
|
||||||
|
// it — this rejects dangling/guessed paths.
|
||||||
|
try {
|
||||||
|
await objectStorage.getObjectEntityFile(objectPath);
|
||||||
|
} catch {
|
||||||
|
res.status(400).json({ error: "object not found", code: "validation" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [agg] = await db
|
||||||
|
.select({
|
||||||
|
maxOrder: sql<
|
||||||
|
number
|
||||||
|
>`coalesce(max(${protocolPhotosTable.sortOrder}), -1)::int`,
|
||||||
|
})
|
||||||
|
.from(protocolPhotosTable)
|
||||||
|
.where(eq(protocolPhotosTable.albumId, albumId));
|
||||||
|
const [row] = await db
|
||||||
|
.insert(protocolPhotosTable)
|
||||||
|
.values({
|
||||||
|
albumId,
|
||||||
|
objectPath,
|
||||||
|
sortOrder: (agg?.maxOrder ?? -1) + 1,
|
||||||
|
uploadedBy: req.session.userId!,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
await logAudit(db, {
|
||||||
|
action: "photo.create",
|
||||||
|
entityType: "photo",
|
||||||
|
entityId: row.id,
|
||||||
|
newValue: row,
|
||||||
|
performedBy: req.session.userId!,
|
||||||
|
});
|
||||||
|
res.status(201).json(row);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/protocol/photos/:id",
|
||||||
|
requireMutate,
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(protocolPhotosTable)
|
||||||
|
.where(eq(protocolPhotosTable.id, id));
|
||||||
|
if (!existing) {
|
||||||
|
res.status(404).json({ error: "Not found", code: "not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await db.delete(protocolPhotosTable).where(eq(protocolPhotosTable.id, id));
|
||||||
|
await logAudit(db, {
|
||||||
|
action: "photo.delete",
|
||||||
|
entityType: "photo",
|
||||||
|
entityId: id,
|
||||||
|
oldValue: existing,
|
||||||
|
performedBy: req.session.userId!,
|
||||||
|
});
|
||||||
|
res.status(204).end();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import {
|
|||||||
RequestUploadUrlBody,
|
RequestUploadUrlBody,
|
||||||
RequestUploadUrlResponse,
|
RequestUploadUrlResponse,
|
||||||
} from "@workspace/api-zod";
|
} from "@workspace/api-zod";
|
||||||
import { ObjectStorageService, ObjectNotFoundError } from "../lib/objectStorage";
|
import {
|
||||||
|
ObjectStorageService,
|
||||||
|
ObjectNotFoundError,
|
||||||
|
extensionForContentType,
|
||||||
|
} from "../lib/objectStorage";
|
||||||
import { canUserReadObjectPath } from "../lib/objectAuthz";
|
import { canUserReadObjectPath } from "../lib/objectAuthz";
|
||||||
import { requireAuth } from "../middlewares/auth";
|
import { requireAuth } from "../middlewares/auth";
|
||||||
|
|
||||||
@@ -109,6 +113,34 @@ router.get("/storage/objects/*path", requireAuth, async (req: Request, res: Resp
|
|||||||
res.status(response.status);
|
res.status(response.status);
|
||||||
response.headers.forEach((value, key) => res.setHeader(key, value));
|
response.headers.forEach((value, key) => res.setHeader(key, value));
|
||||||
|
|
||||||
|
// Opt-in force-download: `?download=1` serves the object as an
|
||||||
|
// attachment (Content-Disposition) so browsers save it instead of
|
||||||
|
// rendering inline. An optional `name` base is combined with the
|
||||||
|
// extension derived from the object's content type.
|
||||||
|
if (req.query.download !== undefined) {
|
||||||
|
const contentType = response.headers.get("content-type") ?? undefined;
|
||||||
|
const ext = extensionForContentType(contentType);
|
||||||
|
const baseRaw =
|
||||||
|
typeof req.query.name === "string" && req.query.name.trim()
|
||||||
|
? req.query.name
|
||||||
|
: (wildcardPath.split("/").pop() ?? "download");
|
||||||
|
// Strip characters that would break the header or the filesystem.
|
||||||
|
const safeBase = baseRaw
|
||||||
|
.replace(/[\r\n"\\/]/g, "")
|
||||||
|
.replace(/[\u0000-\u001f]/g, "")
|
||||||
|
.trim()
|
||||||
|
.slice(0, 180) || "download";
|
||||||
|
const filename = `${safeBase}.${ext}`;
|
||||||
|
const asciiFallback =
|
||||||
|
filename.replace(/[^\x20-\x7e]/g, "_") || `download.${ext}`;
|
||||||
|
res.setHeader(
|
||||||
|
"Content-Disposition",
|
||||||
|
`attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodeURIComponent(
|
||||||
|
filename,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.body) {
|
if (response.body) {
|
||||||
const nodeStream = Readable.fromWeb(response.body as ReadableStream<Uint8Array>);
|
const nodeStream = Readable.fromWeb(response.body as ReadableStream<Uint8Array>);
|
||||||
nodeStream.pipe(res);
|
nodeStream.pipe(res);
|
||||||
|
|||||||
@@ -1669,8 +1669,9 @@
|
|||||||
"dashboard": "لوحة المعلومات",
|
"dashboard": "لوحة المعلومات",
|
||||||
"bookings": "حجوزات القاعات",
|
"bookings": "حجوزات القاعات",
|
||||||
"external": "اللقاءات الخارجية",
|
"external": "اللقاءات الخارجية",
|
||||||
"gifts": "الهدايا والدروع",
|
"gifts": "الإهداءات",
|
||||||
"issues": "طلبات الصرف",
|
"issues": "طلبات الصرف",
|
||||||
|
"photos": "تصوير",
|
||||||
"reports": "التقارير",
|
"reports": "التقارير",
|
||||||
"audit": "سجل النشاط",
|
"audit": "سجل النشاط",
|
||||||
"rooms": "القاعات"
|
"rooms": "القاعات"
|
||||||
@@ -1766,6 +1767,21 @@
|
|||||||
"occasion": "المناسبة",
|
"occasion": "المناسبة",
|
||||||
"quantity": "الكمية"
|
"quantity": "الكمية"
|
||||||
},
|
},
|
||||||
|
"photos": {
|
||||||
|
"newAlbum": "ألبوم جديد",
|
||||||
|
"editAlbum": "تعديل الألبوم",
|
||||||
|
"albumName": "اسم الألبوم",
|
||||||
|
"linkMeeting": "ربط بلقاء خارجي",
|
||||||
|
"noMeeting": "بدون ربط",
|
||||||
|
"addPhotos": "إضافة صور",
|
||||||
|
"download": "تنزيل",
|
||||||
|
"downloadAll": "تنزيل الكل",
|
||||||
|
"uploading": "جارٍ الرفع",
|
||||||
|
"emptyAlbums": "لا توجد ألبومات.",
|
||||||
|
"emptyPhotos": "لا توجد صور في هذا الألبوم.",
|
||||||
|
"confirmDeleteAlbum": "سيتم حذف الألبوم وجميع صوره. لا يمكن التراجع.",
|
||||||
|
"confirmDeletePhoto": "سيتم حذف هذه الصورة. لا يمكن التراجع."
|
||||||
|
},
|
||||||
"reports": {
|
"reports": {
|
||||||
"byRoom": "الحجوزات حسب القاعة",
|
"byRoom": "الحجوزات حسب القاعة",
|
||||||
"byGift": "الصرف حسب الصنف",
|
"byGift": "الصرف حسب الصنف",
|
||||||
|
|||||||
@@ -1542,8 +1542,9 @@
|
|||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"bookings": "Room Bookings",
|
"bookings": "Room Bookings",
|
||||||
"external": "External Meetings",
|
"external": "External Meetings",
|
||||||
"gifts": "Gifts & Shields",
|
"gifts": "Dedications",
|
||||||
"issues": "Issuance Requests",
|
"issues": "Issuance Requests",
|
||||||
|
"photos": "Photos",
|
||||||
"reports": "Reports",
|
"reports": "Reports",
|
||||||
"audit": "Activity Log",
|
"audit": "Activity Log",
|
||||||
"rooms": "Rooms"
|
"rooms": "Rooms"
|
||||||
@@ -1639,6 +1640,21 @@
|
|||||||
"occasion": "Occasion",
|
"occasion": "Occasion",
|
||||||
"quantity": "Quantity"
|
"quantity": "Quantity"
|
||||||
},
|
},
|
||||||
|
"photos": {
|
||||||
|
"newAlbum": "New album",
|
||||||
|
"editAlbum": "Edit album",
|
||||||
|
"albumName": "Album name",
|
||||||
|
"linkMeeting": "Link to external meeting",
|
||||||
|
"noMeeting": "No link",
|
||||||
|
"addPhotos": "Add photos",
|
||||||
|
"download": "Download",
|
||||||
|
"downloadAll": "Download all",
|
||||||
|
"uploading": "Uploading",
|
||||||
|
"emptyAlbums": "No albums yet.",
|
||||||
|
"emptyPhotos": "No photos in this album.",
|
||||||
|
"confirmDeleteAlbum": "This deletes the album and all its photos. This cannot be undone.",
|
||||||
|
"confirmDeletePhoto": "This photo will be deleted. This cannot be undone."
|
||||||
|
},
|
||||||
"reports": {
|
"reports": {
|
||||||
"byRoom": "Bookings by room",
|
"byRoom": "Bookings by room",
|
||||||
"byGift": "Issuance by item",
|
"byGift": "Issuance by item",
|
||||||
|
|||||||
@@ -4637,7 +4637,7 @@ function MeetingRow({
|
|||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key="meeting"
|
key="meeting"
|
||||||
className={`border border-gray-300 px-3 py-3 align-middle text-center text-[#0B1E3F] relative group`}
|
className={`border border-gray-300 px-3 py-3 align-middle text-center ${meeting.isExternal ? "text-[#dc2626]" : "text-[#0B1E3F]"} relative group`}
|
||||||
style={tintedCellStyle(col, true)}
|
style={tintedCellStyle(col, true)}
|
||||||
>
|
>
|
||||||
<EditableCell
|
<EditableCell
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ import {
|
|||||||
List as ListIcon,
|
List as ListIcon,
|
||||||
Link2,
|
Link2,
|
||||||
Copy,
|
Copy,
|
||||||
|
Camera,
|
||||||
|
Upload,
|
||||||
|
Loader2,
|
||||||
|
Download,
|
||||||
|
Image as ImageIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -54,6 +59,7 @@ import {
|
|||||||
import { AppDock } from "@/components/app-dock";
|
import { AppDock } from "@/components/app-dock";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiJson, ApiError } from "@/lib/api-json";
|
import { apiJson, ApiError } from "@/lib/api-json";
|
||||||
|
import { useUpload, type UploadResponse } from "@workspace/object-storage-web";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const API = `${import.meta.env.BASE_URL}api`;
|
const API = `${import.meta.env.BASE_URL}api`;
|
||||||
@@ -185,6 +191,7 @@ type TabKey =
|
|||||||
| "external"
|
| "external"
|
||||||
| "gifts"
|
| "gifts"
|
||||||
| "issues"
|
| "issues"
|
||||||
|
| "photos"
|
||||||
| "reports"
|
| "reports"
|
||||||
| "audit";
|
| "audit";
|
||||||
|
|
||||||
@@ -196,6 +203,7 @@ const TAB_SLUGS: Record<TabKey, string> = {
|
|||||||
external: "external-meetings",
|
external: "external-meetings",
|
||||||
gifts: "gifts",
|
gifts: "gifts",
|
||||||
issues: "gift-issues",
|
issues: "gift-issues",
|
||||||
|
photos: "photos",
|
||||||
reports: "reports",
|
reports: "reports",
|
||||||
audit: "audit",
|
audit: "audit",
|
||||||
};
|
};
|
||||||
@@ -501,6 +509,7 @@ export default function ProtocolPage() {
|
|||||||
{ key: "external", label: t("protocol.tabs.external"), icon: Handshake },
|
{ key: "external", label: t("protocol.tabs.external"), icon: Handshake },
|
||||||
{ key: "gifts", label: t("protocol.tabs.gifts"), icon: Gift },
|
{ key: "gifts", label: t("protocol.tabs.gifts"), icon: Gift },
|
||||||
{ key: "issues", label: t("protocol.tabs.issues"), icon: PackageCheck },
|
{ key: "issues", label: t("protocol.tabs.issues"), icon: PackageCheck },
|
||||||
|
{ key: "photos", label: t("protocol.tabs.photos"), icon: Camera },
|
||||||
{ key: "reports", label: t("protocol.tabs.reports"), icon: BarChart3 },
|
{ key: "reports", label: t("protocol.tabs.reports"), icon: BarChart3 },
|
||||||
...(c?.canViewAudit
|
...(c?.canViewAudit
|
||||||
? [{ key: "audit" as TabKey, label: t("protocol.tabs.audit"), icon: ScrollText }]
|
? [{ key: "audit" as TabKey, label: t("protocol.tabs.audit"), icon: ScrollText }]
|
||||||
@@ -1077,6 +1086,15 @@ export default function ProtocolPage() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{tab === "photos" && (
|
||||||
|
<PhotosView
|
||||||
|
canMutate={!!c?.canMutate}
|
||||||
|
isAr={isAr}
|
||||||
|
externalMeetings={external.data ?? []}
|
||||||
|
onApiError={onApiError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{tab === "reports" && (
|
{tab === "reports" && (
|
||||||
<ReportsView data={reports.data} nameOf={nameOf} t={t} />
|
<ReportsView data={reports.data} nameOf={nameOf} t={t} />
|
||||||
)}
|
)}
|
||||||
@@ -1939,3 +1957,468 @@ function IssueDialog({
|
|||||||
</DialogShell>
|
</DialogShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Visit photos (تصوير) — album grid + album detail with upload gallery.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
type Album = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
externalMeetingId: number | null;
|
||||||
|
meetingTitle: string | null;
|
||||||
|
createdBy: number | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
photoCount: number;
|
||||||
|
coverPath: string | null;
|
||||||
|
};
|
||||||
|
type Photo = {
|
||||||
|
id: number;
|
||||||
|
albumId: number;
|
||||||
|
objectPath: string;
|
||||||
|
sortOrder: number;
|
||||||
|
uploadedBy: number | null;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the authorized image URL from a stored `/objects/<id>` path.
|
||||||
|
const photoSrc = (objectPath: string) => `${API}/storage${objectPath}`;
|
||||||
|
// Force-download URL for a single photo (attachment via ?download=1). The
|
||||||
|
// optional name base gets an extension appended server-side.
|
||||||
|
const photoDownloadSrc = (objectPath: string, name?: string) => {
|
||||||
|
const base = `${API}/storage${objectPath}?download=1`;
|
||||||
|
return name ? `${base}&name=${encodeURIComponent(name)}` : base;
|
||||||
|
};
|
||||||
|
// ZIP download URL for a whole album.
|
||||||
|
const albumDownloadSrc = (albumId: number) =>
|
||||||
|
`${API}/protocol/photo-albums/${albumId}/download`;
|
||||||
|
|
||||||
|
function PhotosView({
|
||||||
|
canMutate,
|
||||||
|
isAr,
|
||||||
|
externalMeetings,
|
||||||
|
onApiError,
|
||||||
|
}: {
|
||||||
|
canMutate: boolean;
|
||||||
|
isAr: boolean;
|
||||||
|
externalMeetings: ExternalMeeting[];
|
||||||
|
onApiError: (e: unknown) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const [openAlbumId, setOpenAlbumId] = useState<number | null>(null);
|
||||||
|
const [albumDialog, setAlbumDialog] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
edit?: Album;
|
||||||
|
}>({ open: false });
|
||||||
|
const [deleteAlbum, setDeleteAlbum] = useState<Album | null>(null);
|
||||||
|
const [deletePhoto, setDeletePhoto] = useState<Photo | null>(null);
|
||||||
|
const BackIcon = isAr ? ArrowRight : ArrowLeft;
|
||||||
|
|
||||||
|
const albums = useQuery({
|
||||||
|
queryKey: ["protocol", "photo-albums"],
|
||||||
|
queryFn: () => apiJson<Album[]>(`${API}/protocol/photo-albums`),
|
||||||
|
});
|
||||||
|
const photos = useQuery({
|
||||||
|
queryKey: ["protocol", "photos", openAlbumId],
|
||||||
|
queryFn: () =>
|
||||||
|
apiJson<Photo[]>(`${API}/protocol/photo-albums/${openAlbumId}/photos`),
|
||||||
|
enabled: openAlbumId !== null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const openAlbum =
|
||||||
|
openAlbumId !== null
|
||||||
|
? (albums.data ?? []).find((a) => a.id === openAlbumId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const { uploadFile, isUploading, progress } = useUpload({
|
||||||
|
basePath: `${API}/storage`,
|
||||||
|
onError: onApiError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalize = useMutation({
|
||||||
|
mutationFn: (objectPath: string) =>
|
||||||
|
apiJson(`${API}/protocol/photo-albums/${openAlbumId}/photos`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ objectPath }),
|
||||||
|
}),
|
||||||
|
onError: onApiError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFiles = async (files: FileList | null) => {
|
||||||
|
if (!files || openAlbumId === null) return;
|
||||||
|
for (const file of Array.from(files)) {
|
||||||
|
const resp: UploadResponse | null = await uploadFile(file);
|
||||||
|
if (resp) await finalize.mutateAsync(resp.objectPath);
|
||||||
|
}
|
||||||
|
await qc.invalidateQueries({ queryKey: ["protocol", "photos", openAlbumId] });
|
||||||
|
await qc.invalidateQueries({ queryKey: ["protocol", "photo-albums"] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const delAlbumMut = useMutation({
|
||||||
|
mutationFn: (id: number) =>
|
||||||
|
apiJson(`${API}/protocol/photo-albums/${id}`, { method: "DELETE" }),
|
||||||
|
onSuccess: () => {
|
||||||
|
setDeleteAlbum(null);
|
||||||
|
qc.invalidateQueries({ queryKey: ["protocol", "photo-albums"] });
|
||||||
|
},
|
||||||
|
onError: onApiError,
|
||||||
|
});
|
||||||
|
const delPhotoMut = useMutation({
|
||||||
|
mutationFn: (id: number) =>
|
||||||
|
apiJson(`${API}/protocol/photos/${id}`, { method: "DELETE" }),
|
||||||
|
onSuccess: () => {
|
||||||
|
setDeletePhoto(null);
|
||||||
|
qc.invalidateQueries({ queryKey: ["protocol", "photos", openAlbumId] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["protocol", "photo-albums"] });
|
||||||
|
},
|
||||||
|
onError: onApiError,
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Album detail view -----------------------------------------------------
|
||||||
|
if (openAlbum) {
|
||||||
|
return (
|
||||||
|
<section className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setOpenAlbumId(null)}
|
||||||
|
>
|
||||||
|
<BackIcon size={16} />
|
||||||
|
</Button>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h2 className="font-semibold text-slate-700 truncate">
|
||||||
|
{openAlbum.name}
|
||||||
|
</h2>
|
||||||
|
{openAlbum.meetingTitle && (
|
||||||
|
<div className="text-xs text-slate-400 truncate">
|
||||||
|
<Link2 size={11} className="inline me-1" />
|
||||||
|
{openAlbum.meetingTitle}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
{(photos.data?.length ?? 0) > 0 ? (
|
||||||
|
<a
|
||||||
|
href={albumDownloadSrc(openAlbum.id)}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-200 bg-white text-slate-700 text-sm hover:bg-slate-50 transition"
|
||||||
|
>
|
||||||
|
<Download size={16} />
|
||||||
|
{t("protocol.photos.downloadAll")}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
aria-disabled="true"
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-200 bg-white text-slate-400 text-sm opacity-60 cursor-not-allowed pointer-events-none"
|
||||||
|
>
|
||||||
|
<Download size={16} />
|
||||||
|
{t("protocol.photos.downloadAll")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{canMutate && (
|
||||||
|
<label className="cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
disabled={isUploading}
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
void handleFiles(e.target.files);
|
||||||
|
e.currentTarget.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-primary text-primary-foreground text-sm hover:opacity-90 transition">
|
||||||
|
{isUploading ? (
|
||||||
|
<Loader2 size={16} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Upload size={16} />
|
||||||
|
)}
|
||||||
|
{isUploading
|
||||||
|
? `${t("protocol.photos.uploading")} ${progress}%`
|
||||||
|
: t("protocol.photos.addPhotos")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{photos.data && photos.data.length === 0 && (
|
||||||
|
<EmptyState text={t("protocol.photos.emptyPhotos")} />
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
|
||||||
|
{(photos.data ?? []).map((p) => (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className="group relative aspect-square rounded-xl overflow-hidden border border-slate-200 bg-slate-100"
|
||||||
|
>
|
||||||
|
<a href={photoSrc(p.objectPath)} target="_blank" rel="noreferrer">
|
||||||
|
<img
|
||||||
|
src={photoSrc(p.objectPath)}
|
||||||
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={photoDownloadSrc(
|
||||||
|
p.objectPath,
|
||||||
|
`${openAlbum.name}-${p.sortOrder + 1}`,
|
||||||
|
)}
|
||||||
|
download
|
||||||
|
title={t("protocol.photos.download")}
|
||||||
|
aria-label={t("protocol.photos.download")}
|
||||||
|
className="absolute bottom-1 end-1 p-1.5 rounded-lg bg-black/50 text-white opacity-0 group-hover:opacity-100 transition"
|
||||||
|
>
|
||||||
|
<Download size={14} />
|
||||||
|
</a>
|
||||||
|
{canMutate && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setDeletePhoto(p)}
|
||||||
|
className="absolute top-1 end-1 p-1.5 rounded-lg bg-black/50 text-white opacity-0 group-hover:opacity-100 transition"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{albumDialog.open && (
|
||||||
|
<AlbumDialog
|
||||||
|
edit={albumDialog.edit}
|
||||||
|
externalMeetings={externalMeetings}
|
||||||
|
onClose={() => setAlbumDialog({ open: false })}
|
||||||
|
onSaved={() => {
|
||||||
|
setAlbumDialog({ open: false });
|
||||||
|
qc.invalidateQueries({ queryKey: ["protocol", "photo-albums"] });
|
||||||
|
}}
|
||||||
|
onError={onApiError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<AlertDialog
|
||||||
|
open={deletePhoto !== null}
|
||||||
|
onOpenChange={(o) => !o && setDeletePhoto(null)}
|
||||||
|
>
|
||||||
|
<AlertDialogContent dir={isAr ? "rtl" : "ltr"}>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{t("common.confirmDelete")}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t("protocol.photos.confirmDeletePhoto")}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-rose-600 hover:bg-rose-700"
|
||||||
|
onClick={() => deletePhoto && delPhotoMut.mutate(deletePhoto.id)}
|
||||||
|
>
|
||||||
|
{t("common.delete")}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Album grid view -------------------------------------------------------
|
||||||
|
return (
|
||||||
|
<section className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="font-semibold text-slate-700">
|
||||||
|
{t("protocol.tabs.photos")}
|
||||||
|
</h2>
|
||||||
|
{canMutate && (
|
||||||
|
<Button size="sm" onClick={() => setAlbumDialog({ open: true })}>
|
||||||
|
<Plus size={16} className="me-1" />
|
||||||
|
{t("protocol.photos.newAlbum")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{albums.data && albums.data.length === 0 && (
|
||||||
|
<EmptyState text={t("protocol.photos.emptyAlbums")} />
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
||||||
|
{(albums.data ?? []).map((a) => (
|
||||||
|
<div
|
||||||
|
key={a.id}
|
||||||
|
className="bg-white rounded-xl border border-slate-200 overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenAlbumId(a.id)}
|
||||||
|
className="block w-full aspect-square bg-slate-100 relative"
|
||||||
|
>
|
||||||
|
{a.coverPath ? (
|
||||||
|
<img
|
||||||
|
src={photoSrc(a.coverPath)}
|
||||||
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="absolute inset-0 flex items-center justify-center text-slate-300">
|
||||||
|
<ImageIcon size={32} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="absolute bottom-1 end-1 text-[11px] px-1.5 py-0.5 rounded-full bg-black/55 text-white">
|
||||||
|
{a.photoCount}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div className="p-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenAlbumId(a.id)}
|
||||||
|
className="block w-full text-start font-medium text-sm text-slate-800 truncate"
|
||||||
|
>
|
||||||
|
{a.name}
|
||||||
|
</button>
|
||||||
|
{a.meetingTitle && (
|
||||||
|
<div className="text-[11px] text-slate-400 truncate">
|
||||||
|
<Link2 size={10} className="inline me-1" />
|
||||||
|
{a.meetingTitle}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{canMutate && (
|
||||||
|
<div className="flex gap-1 mt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 px-2"
|
||||||
|
onClick={() => setAlbumDialog({ open: true, edit: a })}
|
||||||
|
>
|
||||||
|
<Pencil size={13} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 px-2"
|
||||||
|
onClick={() => setDeleteAlbum(a)}
|
||||||
|
>
|
||||||
|
<Trash2 size={13} className="text-rose-500" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{albumDialog.open && (
|
||||||
|
<AlbumDialog
|
||||||
|
edit={albumDialog.edit}
|
||||||
|
externalMeetings={externalMeetings}
|
||||||
|
onClose={() => setAlbumDialog({ open: false })}
|
||||||
|
onSaved={() => {
|
||||||
|
setAlbumDialog({ open: false });
|
||||||
|
qc.invalidateQueries({ queryKey: ["protocol", "photo-albums"] });
|
||||||
|
}}
|
||||||
|
onError={onApiError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<AlertDialog
|
||||||
|
open={deleteAlbum !== null}
|
||||||
|
onOpenChange={(o) => !o && setDeleteAlbum(null)}
|
||||||
|
>
|
||||||
|
<AlertDialogContent dir={isAr ? "rtl" : "ltr"}>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{t("common.confirmDelete")}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t("protocol.photos.confirmDeleteAlbum")}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-rose-600 hover:bg-rose-700"
|
||||||
|
onClick={() => deleteAlbum && delAlbumMut.mutate(deleteAlbum.id)}
|
||||||
|
>
|
||||||
|
{t("common.delete")}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlbumDialog({
|
||||||
|
edit,
|
||||||
|
externalMeetings,
|
||||||
|
onClose,
|
||||||
|
onSaved,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
edit?: Album;
|
||||||
|
externalMeetings: ExternalMeeting[];
|
||||||
|
onClose: () => void;
|
||||||
|
onSaved: () => void;
|
||||||
|
onError: (e: unknown) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [name, setName] = useState(edit?.name ?? "");
|
||||||
|
const [meetingId, setMeetingId] = useState<string>(
|
||||||
|
edit?.externalMeetingId != null ? String(edit.externalMeetingId) : "none",
|
||||||
|
);
|
||||||
|
|
||||||
|
const save = useMutation({
|
||||||
|
mutationFn: () => {
|
||||||
|
const payload = {
|
||||||
|
name: name.trim(),
|
||||||
|
externalMeetingId: meetingId === "none" ? null : Number(meetingId),
|
||||||
|
};
|
||||||
|
return edit
|
||||||
|
? apiJson(`${API}/protocol/photo-albums/${edit.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
: apiJson(`${API}/protocol/photo-albums`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: onSaved,
|
||||||
|
onError,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogShell
|
||||||
|
title={edit ? t("protocol.photos.editAlbum") : t("protocol.photos.newAlbum")}
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={() => save.mutate()}
|
||||||
|
submitLabel={t("common.save")}
|
||||||
|
saving={save.isPending}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label>{t("protocol.photos.albumName")}</Label>
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>{t("protocol.photos.linkMeeting")}</Label>
|
||||||
|
<Select value={meetingId} onValueChange={setMeetingId}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">{t("protocol.photos.noMeeting")}</SelectItem>
|
||||||
|
{externalMeetings.map((m) => (
|
||||||
|
<SelectItem key={m.id} value={String(m.id)}>
|
||||||
|
{m.title}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</DialogShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -241,6 +241,67 @@ export const protocolGiftIssuesTable = pgTable(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Visit photo albums (صور الزيارات) — "تصوير" tab.
|
||||||
|
//
|
||||||
|
// An album groups photographs of a visit. It may optionally be linked to an
|
||||||
|
// external protocol meeting (اللقاءات الخارجية); the link is `set null` so
|
||||||
|
// deleting the meeting never removes the album. The album cover is derived at
|
||||||
|
// query time from the first photo (by sortOrder), so there is no circular
|
||||||
|
// album <-> photo foreign key to keep in sync.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export const protocolPhotoAlbumsTable = pgTable(
|
||||||
|
"protocol_photo_albums",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
name: varchar("name", { length: 300 }).notNull(),
|
||||||
|
externalMeetingId: integer("external_meeting_id").references(
|
||||||
|
() => protocolExternalMeetingsTable.id,
|
||||||
|
{ onDelete: "set null" },
|
||||||
|
),
|
||||||
|
createdBy: integer("created_by").references(() => usersTable.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow()
|
||||||
|
.$onUpdate(() => new Date()),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
meetingIdx: index("protocol_albums_meeting_idx").on(t.externalMeetingId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Photos inside an album. Each photo references a private object-storage path
|
||||||
|
// (`/objects/<id>`) served through the authorized object endpoint. Deleting an
|
||||||
|
// album cascades to its photos.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export const protocolPhotosTable = pgTable(
|
||||||
|
"protocol_photos",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
albumId: integer("album_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => protocolPhotoAlbumsTable.id, { onDelete: "cascade" }),
|
||||||
|
objectPath: varchar("object_path", { length: 500 }).notNull(),
|
||||||
|
sortOrder: integer("sort_order").notNull().default(0),
|
||||||
|
uploadedBy: integer("uploaded_by").references(() => usersTable.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
albumIdx: index("protocol_photos_album_idx").on(t.albumId),
|
||||||
|
pathIdx: index("protocol_photos_path_idx").on(t.objectPath),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Audit log (scoped to the protocol module only)
|
// Audit log (scoped to the protocol module only)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -306,4 +367,18 @@ export type InsertProtocolGiftIssue = z.infer<
|
|||||||
>;
|
>;
|
||||||
export type ProtocolGiftIssue = typeof protocolGiftIssuesTable.$inferSelect;
|
export type ProtocolGiftIssue = typeof protocolGiftIssuesTable.$inferSelect;
|
||||||
|
|
||||||
|
export const insertProtocolPhotoAlbumSchema = createInsertSchema(
|
||||||
|
protocolPhotoAlbumsTable,
|
||||||
|
).omit({ id: true, createdAt: true, updatedAt: true });
|
||||||
|
export type InsertProtocolPhotoAlbum = z.infer<
|
||||||
|
typeof insertProtocolPhotoAlbumSchema
|
||||||
|
>;
|
||||||
|
export type ProtocolPhotoAlbum = typeof protocolPhotoAlbumsTable.$inferSelect;
|
||||||
|
|
||||||
|
export const insertProtocolPhotoSchema = createInsertSchema(
|
||||||
|
protocolPhotosTable,
|
||||||
|
).omit({ id: true, createdAt: true });
|
||||||
|
export type InsertProtocolPhoto = z.infer<typeof insertProtocolPhotoSchema>;
|
||||||
|
export type ProtocolPhoto = typeof protocolPhotosTable.$inferSelect;
|
||||||
|
|
||||||
export type ProtocolAuditLog = typeof protocolAuditLogsTable.$inferSelect;
|
export type ProtocolAuditLog = typeof protocolAuditLogsTable.$inferSelect;
|
||||||
|
|||||||
Generated
+378
@@ -90,6 +90,9 @@ importers:
|
|||||||
arabic-persian-reshaper:
|
arabic-persian-reshaper:
|
||||||
specifier: 1.0.1
|
specifier: 1.0.1
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
|
archiver:
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0
|
||||||
bcryptjs:
|
bcryptjs:
|
||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
@@ -148,6 +151,9 @@ importers:
|
|||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/archiver':
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0
|
||||||
'@types/bcryptjs':
|
'@types/bcryptjs':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
@@ -2434,6 +2440,7 @@ packages:
|
|||||||
'@smithy/core@3.24.1':
|
'@smithy/core@3.24.1':
|
||||||
resolution: {integrity: sha512-3mT7o4qQyUWttYnVK3A0Z/u3Xha3E81tXn32Tz6vjZiUXhBrkEivpw1hBYfh84iFF9CSzkBU9Y1DJ3Q6RQ231g==}
|
resolution: {integrity: sha512-3mT7o4qQyUWttYnVK3A0Z/u3Xha3E81tXn32Tz6vjZiUXhBrkEivpw1hBYfh84iFF9CSzkBU9Y1DJ3Q6RQ231g==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Deprecated due to bug in browser bundling instructions https://github.com/smithy-lang/smithy-typescript/issues/2025
|
||||||
|
|
||||||
'@smithy/credential-provider-imds@4.3.1':
|
'@smithy/credential-provider-imds@4.3.1':
|
||||||
resolution: {integrity: sha512-0S/acwHnqX4WrjXzhdiDRxsG2s9SC0cpPIK9nZ1R6UOHd+j7uL28+4bHu22urbLk2TVw3fkp6na/+fkUt/pLNQ==}
|
resolution: {integrity: sha512-0S/acwHnqX4WrjXzhdiDRxsG2s9SC0cpPIK9nZ1R6UOHd+j7uL28+4bHu22urbLk2TVw3fkp6na/+fkUt/pLNQ==}
|
||||||
@@ -2888,6 +2895,9 @@ packages:
|
|||||||
'@transloadit/prettier-bytes@0.3.5':
|
'@transloadit/prettier-bytes@0.3.5':
|
||||||
resolution: {integrity: sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA==}
|
resolution: {integrity: sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA==}
|
||||||
|
|
||||||
|
'@types/archiver@8.0.0':
|
||||||
|
resolution: {integrity: sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A==}
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
|
|
||||||
@@ -2996,6 +3006,9 @@ packages:
|
|||||||
'@types/react@19.2.14':
|
'@types/react@19.2.14':
|
||||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||||
|
|
||||||
|
'@types/readdir-glob@1.1.5':
|
||||||
|
resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==}
|
||||||
|
|
||||||
'@types/retry@0.12.2':
|
'@types/retry@0.12.2':
|
||||||
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
|
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
|
||||||
|
|
||||||
@@ -3101,6 +3114,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
|
engines: {node: '>=6.5'}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -3143,6 +3160,10 @@ packages:
|
|||||||
arabic-persian-reshaper@1.0.1:
|
arabic-persian-reshaper@1.0.1:
|
||||||
resolution: {integrity: sha512-VYBjkhz6o4W1Xt4mD2LAReljJpLSw5CUZMqSBDIQRvFgUSlTKEYghapgBWvkeMWF4W+KF3Fm+/z8EywJU4PBeg==}
|
resolution: {integrity: sha512-VYBjkhz6o4W1Xt4mD2LAReljJpLSw5CUZMqSBDIQRvFgUSlTKEYghapgBWvkeMWF4W+KF3Fm+/z8EywJU4PBeg==}
|
||||||
|
|
||||||
|
archiver@8.0.0:
|
||||||
|
resolution: {integrity: sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
@@ -3153,13 +3174,69 @@ packages:
|
|||||||
asn1.js@5.4.1:
|
asn1.js@5.4.1:
|
||||||
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
|
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
|
||||||
|
|
||||||
|
async@3.2.6:
|
||||||
|
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||||
|
|
||||||
atomic-sleep@1.0.0:
|
atomic-sleep@1.0.0:
|
||||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
b4a@1.8.1:
|
||||||
|
resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}
|
||||||
|
peerDependencies:
|
||||||
|
react-native-b4a: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-native-b4a:
|
||||||
|
optional: true
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
balanced-match@4.0.4:
|
||||||
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
bare-events@2.9.1:
|
||||||
|
resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==}
|
||||||
|
peerDependencies:
|
||||||
|
bare-abort-controller: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bare-abort-controller:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
bare-fs@4.7.3:
|
||||||
|
resolution: {integrity: sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==}
|
||||||
|
engines: {bare: '>=1.16.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bare-buffer: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bare-buffer:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
bare-os@3.9.3:
|
||||||
|
resolution: {integrity: sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==}
|
||||||
|
engines: {bare: '>=1.14.0'}
|
||||||
|
|
||||||
|
bare-path@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==}
|
||||||
|
|
||||||
|
bare-stream@2.13.3:
|
||||||
|
resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==}
|
||||||
|
peerDependencies:
|
||||||
|
bare-abort-controller: '*'
|
||||||
|
bare-buffer: '*'
|
||||||
|
bare-events: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bare-abort-controller:
|
||||||
|
optional: true
|
||||||
|
bare-buffer:
|
||||||
|
optional: true
|
||||||
|
bare-events:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
bare-url@2.4.5:
|
||||||
|
resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==}
|
||||||
|
|
||||||
base64-js@0.0.8:
|
base64-js@0.0.8:
|
||||||
resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
|
resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3196,6 +3273,10 @@ packages:
|
|||||||
brace-expansion@2.0.2:
|
brace-expansion@2.0.2:
|
||||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||||
|
|
||||||
|
brace-expansion@5.0.7:
|
||||||
|
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -3211,12 +3292,19 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-crc32@1.0.0:
|
||||||
|
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1:
|
buffer-equal-constant-time@1.0.1:
|
||||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -3270,6 +3358,10 @@ packages:
|
|||||||
compare-versions@6.1.1:
|
compare-versions@6.1.1:
|
||||||
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
|
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
|
||||||
|
|
||||||
|
compress-commons@7.0.1:
|
||||||
|
resolution: {integrity: sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
connect-pg-simple@10.0.0:
|
connect-pg-simple@10.0.0:
|
||||||
resolution: {integrity: sha512-pBGVazlqiMrackzCr0eKhn4LO5trJXsOX0nQoey9wCOayh80MYtThCbq8eoLsjpiWgiok/h+1/uti9/2/Una8A==}
|
resolution: {integrity: sha512-pBGVazlqiMrackzCr0eKhn4LO5trJXsOX0nQoey9wCOayh80MYtThCbq8eoLsjpiWgiok/h+1/uti9/2/Una8A==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=22.0.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=22.0.0}
|
||||||
@@ -3303,10 +3395,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
core-util-is@1.0.3:
|
||||||
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
cors@2.8.6:
|
cors@2.8.6:
|
||||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
crc-32@1.2.2:
|
||||||
|
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
|
||||||
|
engines: {node: '>=0.8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
crc32-stream@7.0.1:
|
||||||
|
resolution: {integrity: sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -3662,12 +3766,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1:
|
||||||
|
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
eventemitter3@4.0.7:
|
eventemitter3@4.0.7:
|
||||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||||
|
|
||||||
eventemitter3@5.0.4:
|
eventemitter3@5.0.4:
|
||||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||||
|
|
||||||
|
events-universal@1.0.1:
|
||||||
|
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
|
||||||
|
|
||||||
|
events@3.3.0:
|
||||||
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
|
engines: {node: '>=0.8.x'}
|
||||||
|
|
||||||
execa@9.6.1:
|
execa@9.6.1:
|
||||||
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
|
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
|
||||||
engines: {node: ^18.19.0 || >=20.5.0}
|
engines: {node: ^18.19.0 || >=20.5.0}
|
||||||
@@ -3699,6 +3814,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==}
|
resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
fast-fifo@1.3.2:
|
||||||
|
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||||
|
|
||||||
fast-glob@3.3.3:
|
fast-glob@3.3.3:
|
||||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||||
engines: {node: '>=8.6.0'}
|
engines: {node: '>=8.6.0'}
|
||||||
@@ -3873,6 +3991,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ieee754@1.2.1:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
ignore@7.0.5:
|
ignore@7.0.5:
|
||||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@@ -3937,6 +4058,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
isarray@1.0.0:
|
||||||
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
@@ -3984,6 +4108,10 @@ packages:
|
|||||||
jws@4.0.1:
|
jws@4.0.1:
|
||||||
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||||
|
|
||||||
|
lazystream@1.0.1:
|
||||||
|
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||||
|
engines: {node: '>= 0.6.3'}
|
||||||
|
|
||||||
leven@4.1.0:
|
leven@4.1.0:
|
||||||
resolution: {integrity: sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==}
|
resolution: {integrity: sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -4145,6 +4273,10 @@ packages:
|
|||||||
minimalistic-assert@1.0.1:
|
minimalistic-assert@1.0.1:
|
||||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||||
|
|
||||||
|
minimatch@10.2.5:
|
||||||
|
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
minimatch@9.0.9:
|
minimatch@9.0.9:
|
||||||
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
@@ -4201,6 +4333,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==}
|
resolution: {integrity: sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
normalize-path@3.0.0:
|
||||||
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
npm-run-path@6.0.0:
|
npm-run-path@6.0.0:
|
||||||
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4417,9 +4553,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1:
|
||||||
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
process-warning@5.0.0:
|
process-warning@5.0.0:
|
||||||
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||||
|
|
||||||
|
process@0.11.10:
|
||||||
|
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
@@ -4592,6 +4735,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
readable-stream@2.3.8:
|
||||||
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
|
||||||
|
readable-stream@4.7.0:
|
||||||
|
resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
|
readdir-glob@3.0.0:
|
||||||
|
resolution: {integrity: sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
readdirp@4.1.2:
|
readdirp@4.1.2:
|
||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
@@ -4610,6 +4764,7 @@ packages:
|
|||||||
recharts@2.15.4:
|
recharts@2.15.4:
|
||||||
resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==}
|
resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0
|
react-dom: 19.1.0
|
||||||
@@ -4654,6 +4809,9 @@ packages:
|
|||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
safe-buffer@5.1.2:
|
||||||
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
|
|
||||||
safe-buffer@5.2.1:
|
safe-buffer@5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
@@ -4771,10 +4929,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
streamx@2.28.0:
|
||||||
|
resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==}
|
||||||
|
|
||||||
string-argv@0.3.2:
|
string-argv@0.3.2:
|
||||||
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
||||||
engines: {node: '>=0.6.19'}
|
engines: {node: '>=0.6.19'}
|
||||||
|
|
||||||
|
string_decoder@1.1.1:
|
||||||
|
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4805,6 +4972,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tar-stream@3.2.0:
|
||||||
|
resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==}
|
||||||
|
|
||||||
|
teex@1.0.1:
|
||||||
|
resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
|
||||||
|
|
||||||
|
text-decoder@1.2.7:
|
||||||
|
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
|
||||||
|
|
||||||
thread-stream@3.1.0:
|
thread-stream@3.1.0:
|
||||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||||
|
|
||||||
@@ -4829,6 +5005,7 @@ packages:
|
|||||||
tsconfck@3.1.6:
|
tsconfck@3.1.6:
|
||||||
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
|
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
|
||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
|
deprecated: unmaintained
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ^5.0.0
|
typescript: ^5.0.0
|
||||||
@@ -5058,6 +5235,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
zip-stream@7.0.5:
|
||||||
|
resolution: {integrity: sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
zod@3.25.76:
|
zod@3.25.76:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
@@ -7417,6 +7598,11 @@ snapshots:
|
|||||||
|
|
||||||
'@transloadit/prettier-bytes@0.3.5': {}
|
'@transloadit/prettier-bytes@0.3.5': {}
|
||||||
|
|
||||||
|
'@types/archiver@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.3.5
|
||||||
|
'@types/readdir-glob': 1.1.5
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.0
|
'@babel/parser': 7.29.0
|
||||||
@@ -7548,6 +7734,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/readdir-glob@1.1.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.3.5
|
||||||
|
|
||||||
'@types/retry@0.12.2': {}
|
'@types/retry@0.12.2': {}
|
||||||
|
|
||||||
'@types/sanitize-html@2.16.1':
|
'@types/sanitize-html@2.16.1':
|
||||||
@@ -7673,6 +7863,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
event-target-shim: 5.0.1
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
@@ -7708,6 +7902,22 @@ snapshots:
|
|||||||
|
|
||||||
arabic-persian-reshaper@1.0.1: {}
|
arabic-persian-reshaper@1.0.1: {}
|
||||||
|
|
||||||
|
archiver@8.0.0:
|
||||||
|
dependencies:
|
||||||
|
async: 3.2.6
|
||||||
|
buffer-crc32: 1.0.0
|
||||||
|
is-stream: 4.0.1
|
||||||
|
lazystream: 1.0.1
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
readable-stream: 4.7.0
|
||||||
|
readdir-glob: 3.0.0
|
||||||
|
tar-stream: 3.2.0
|
||||||
|
zip-stream: 7.0.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bare-abort-controller
|
||||||
|
- bare-buffer
|
||||||
|
- react-native-b4a
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
aria-hidden@1.2.6:
|
aria-hidden@1.2.6:
|
||||||
@@ -7721,10 +7931,49 @@ snapshots:
|
|||||||
minimalistic-assert: 1.0.1
|
minimalistic-assert: 1.0.1
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
async@3.2.6: {}
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
atomic-sleep@1.0.0: {}
|
||||||
|
|
||||||
|
b4a@1.8.1: {}
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
|
bare-events@2.9.1: {}
|
||||||
|
|
||||||
|
bare-fs@4.7.3:
|
||||||
|
dependencies:
|
||||||
|
bare-events: 2.9.1
|
||||||
|
bare-path: 3.0.1
|
||||||
|
bare-stream: 2.13.3(bare-events@2.9.1)
|
||||||
|
bare-url: 2.4.5
|
||||||
|
fast-fifo: 1.3.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bare-abort-controller
|
||||||
|
- react-native-b4a
|
||||||
|
|
||||||
|
bare-os@3.9.3: {}
|
||||||
|
|
||||||
|
bare-path@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
bare-os: 3.9.3
|
||||||
|
|
||||||
|
bare-stream@2.13.3(bare-events@2.9.1):
|
||||||
|
dependencies:
|
||||||
|
b4a: 1.8.1
|
||||||
|
streamx: 2.28.0
|
||||||
|
teex: 1.0.1
|
||||||
|
optionalDependencies:
|
||||||
|
bare-events: 2.9.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- react-native-b4a
|
||||||
|
|
||||||
|
bare-url@2.4.5:
|
||||||
|
dependencies:
|
||||||
|
bare-path: 3.0.1
|
||||||
|
|
||||||
base64-js@0.0.8: {}
|
base64-js@0.0.8: {}
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
@@ -7761,6 +8010,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
|
brace-expansion@5.0.7:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 4.0.4
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range: 7.1.1
|
fill-range: 7.1.1
|
||||||
@@ -7781,10 +8034,17 @@ snapshots:
|
|||||||
node-releases: 2.0.36
|
node-releases: 2.0.36
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||||
|
|
||||||
|
buffer-crc32@1.0.0: {}
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
call-bind-apply-helpers@1.0.2:
|
||||||
@@ -7835,6 +8095,14 @@ snapshots:
|
|||||||
|
|
||||||
compare-versions@6.1.1: {}
|
compare-versions@6.1.1: {}
|
||||||
|
|
||||||
|
compress-commons@7.0.1:
|
||||||
|
dependencies:
|
||||||
|
crc-32: 1.2.2
|
||||||
|
crc32-stream: 7.0.1
|
||||||
|
is-stream: 4.0.1
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
readable-stream: 4.7.0
|
||||||
|
|
||||||
connect-pg-simple@10.0.0:
|
connect-pg-simple@10.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pg: 8.20.0
|
pg: 8.20.0
|
||||||
@@ -7860,11 +8128,20 @@ snapshots:
|
|||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
cors@2.8.6:
|
cors@2.8.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
|
|
||||||
|
crc-32@1.2.2: {}
|
||||||
|
|
||||||
|
crc32-stream@7.0.1:
|
||||||
|
dependencies:
|
||||||
|
crc-32: 1.2.2
|
||||||
|
readable-stream: 4.7.0
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -8182,10 +8459,20 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1: {}
|
||||||
|
|
||||||
eventemitter3@4.0.7: {}
|
eventemitter3@4.0.7: {}
|
||||||
|
|
||||||
eventemitter3@5.0.4: {}
|
eventemitter3@5.0.4: {}
|
||||||
|
|
||||||
|
events-universal@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
bare-events: 2.9.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bare-abort-controller
|
||||||
|
|
||||||
|
events@3.3.0: {}
|
||||||
|
|
||||||
execa@9.6.1:
|
execa@9.6.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sindresorhus/merge-streams': 4.0.0
|
'@sindresorhus/merge-streams': 4.0.0
|
||||||
@@ -8260,6 +8547,8 @@ snapshots:
|
|||||||
|
|
||||||
fast-equals@5.4.0: {}
|
fast-equals@5.4.0: {}
|
||||||
|
|
||||||
|
fast-fifo@1.3.2: {}
|
||||||
|
|
||||||
fast-glob@3.3.3:
|
fast-glob@3.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@@ -8452,6 +8741,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
@@ -8489,6 +8780,8 @@ snapshots:
|
|||||||
|
|
||||||
is-unicode-supported@2.1.0: {}
|
is-unicode-supported@2.1.0: {}
|
||||||
|
|
||||||
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
@@ -8528,6 +8821,10 @@ snapshots:
|
|||||||
jwa: 2.0.1
|
jwa: 2.0.1
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
lazystream@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 2.3.8
|
||||||
|
|
||||||
leven@4.1.0: {}
|
leven@4.1.0: {}
|
||||||
|
|
||||||
lightningcss-android-arm64@1.31.1:
|
lightningcss-android-arm64@1.31.1:
|
||||||
@@ -8656,6 +8953,10 @@ snapshots:
|
|||||||
|
|
||||||
minimalistic-assert@1.0.1: {}
|
minimalistic-assert@1.0.1: {}
|
||||||
|
|
||||||
|
minimatch@10.2.5:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 5.0.7
|
||||||
|
|
||||||
minimatch@9.0.9:
|
minimatch@9.0.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.2
|
brace-expansion: 2.0.2
|
||||||
@@ -8693,6 +8994,8 @@ snapshots:
|
|||||||
|
|
||||||
nodemailer@8.0.7: {}
|
nodemailer@8.0.7: {}
|
||||||
|
|
||||||
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
npm-run-path@6.0.0:
|
npm-run-path@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 4.0.0
|
path-key: 4.0.0
|
||||||
@@ -8935,8 +9238,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
parse-ms: 4.0.0
|
parse-ms: 4.0.0
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
process-warning@5.0.0: {}
|
process-warning@5.0.0: {}
|
||||||
|
|
||||||
|
process@0.11.10: {}
|
||||||
|
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
@@ -9132,6 +9439,28 @@ snapshots:
|
|||||||
|
|
||||||
react@19.1.0: {}
|
react@19.1.0: {}
|
||||||
|
|
||||||
|
readable-stream@2.3.8:
|
||||||
|
dependencies:
|
||||||
|
core-util-is: 1.0.3
|
||||||
|
inherits: 2.0.4
|
||||||
|
isarray: 1.0.0
|
||||||
|
process-nextick-args: 2.0.1
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
string_decoder: 1.1.1
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
|
readable-stream@4.7.0:
|
||||||
|
dependencies:
|
||||||
|
abort-controller: 3.0.0
|
||||||
|
buffer: 6.0.3
|
||||||
|
events: 3.3.0
|
||||||
|
process: 0.11.10
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
|
||||||
|
readdir-glob@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
minimatch: 10.2.5
|
||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
readdirp@5.0.0: {}
|
readdirp@5.0.0: {}
|
||||||
@@ -9216,6 +9545,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
safe-buffer@5.1.2: {}
|
||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
safe-stable-stringify@2.5.0: {}
|
safe-stable-stringify@2.5.0: {}
|
||||||
@@ -9369,8 +9700,25 @@ snapshots:
|
|||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
|
|
||||||
|
streamx@2.28.0:
|
||||||
|
dependencies:
|
||||||
|
events-universal: 1.0.1
|
||||||
|
fast-fifo: 1.3.2
|
||||||
|
text-decoder: 1.2.7
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bare-abort-controller
|
||||||
|
- react-native-b4a
|
||||||
|
|
||||||
string-argv@0.3.2: {}
|
string-argv@0.3.2: {}
|
||||||
|
|
||||||
|
string_decoder@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
@@ -9391,6 +9739,30 @@ snapshots:
|
|||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
|
|
||||||
|
tar-stream@3.2.0:
|
||||||
|
dependencies:
|
||||||
|
b4a: 1.8.1
|
||||||
|
bare-fs: 4.7.3
|
||||||
|
fast-fifo: 1.3.2
|
||||||
|
streamx: 2.28.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bare-abort-controller
|
||||||
|
- bare-buffer
|
||||||
|
- react-native-b4a
|
||||||
|
|
||||||
|
teex@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
streamx: 2.28.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bare-abort-controller
|
||||||
|
- react-native-b4a
|
||||||
|
|
||||||
|
text-decoder@1.2.7:
|
||||||
|
dependencies:
|
||||||
|
b4a: 1.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- react-native-b4a
|
||||||
|
|
||||||
thread-stream@3.1.0:
|
thread-stream@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
real-require: 0.2.0
|
real-require: 0.2.0
|
||||||
@@ -9590,6 +9962,12 @@ snapshots:
|
|||||||
|
|
||||||
yoctocolors@2.1.2: {}
|
yoctocolors@2.1.2: {}
|
||||||
|
|
||||||
|
zip-stream@7.0.5:
|
||||||
|
dependencies:
|
||||||
|
compress-commons: 7.0.1
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
readable-stream: 4.7.0
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|
||||||
zod@4.3.6: {}
|
zod@4.3.6: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user