hanoman 0.1.24 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/server.js +99 -31
- package/package.json +1 -1
- package/web/assets/{index-DqY-l_Cl.js → index-D68Z05gR.js} +1540 -1540
- package/web/index.html +1 -1
package/dist/build-info.json
CHANGED
package/dist/server.js
CHANGED
|
@@ -26094,10 +26094,64 @@ async function pruneOldTickets(now = Date.now()) {
|
|
|
26094
26094
|
}
|
|
26095
26095
|
}
|
|
26096
26096
|
|
|
26097
|
+
// src/services/ticket-intake.ts
|
|
26098
|
+
init_db();
|
|
26099
|
+
var TICKET_UPLOAD = {
|
|
26100
|
+
MAX_FILES: 3,
|
|
26101
|
+
MAX_BYTES: 5 * 1024 * 1024,
|
|
26102
|
+
OK_MIME: /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"])
|
|
26103
|
+
};
|
|
26104
|
+
async function parseTicketUpload(req) {
|
|
26105
|
+
const fields = {};
|
|
26106
|
+
const files = [];
|
|
26107
|
+
try {
|
|
26108
|
+
for await (const part of req.parts()) {
|
|
26109
|
+
if (part.type === "file") {
|
|
26110
|
+
const buf = await part.toBuffer();
|
|
26111
|
+
if (part.file?.truncated || !TICKET_UPLOAD.OK_MIME.has(part.mimetype) || files.length >= TICKET_UPLOAD.MAX_FILES) continue;
|
|
26112
|
+
files.push({ buf, mime: part.mimetype, name: String(part.filename ?? "gambar") });
|
|
26113
|
+
} else {
|
|
26114
|
+
fields[part.fieldname] = String(part.value ?? "");
|
|
26115
|
+
}
|
|
26116
|
+
}
|
|
26117
|
+
} catch {
|
|
26118
|
+
return null;
|
|
26119
|
+
}
|
|
26120
|
+
return { fields, files };
|
|
26121
|
+
}
|
|
26122
|
+
async function intakeTicket(input) {
|
|
26123
|
+
const { ticket, key } = await createTicket({
|
|
26124
|
+
projectId: input.projectId,
|
|
26125
|
+
category: input.category,
|
|
26126
|
+
title: input.title,
|
|
26127
|
+
detail: input.detail,
|
|
26128
|
+
reporterEmail: input.reporterEmail
|
|
26129
|
+
});
|
|
26130
|
+
await notifySynced("ticket", ticket.id);
|
|
26131
|
+
for (const f of input.files) {
|
|
26132
|
+
const { storageKey, size } = await saveUpload(f.buf, f.mime);
|
|
26133
|
+
const att = await prisma.ticketAttachment.create({
|
|
26134
|
+
data: {
|
|
26135
|
+
ticketId: ticket.id,
|
|
26136
|
+
projectId: input.projectId,
|
|
26137
|
+
filename: f.name.slice(0, 200),
|
|
26138
|
+
mimeType: f.mime,
|
|
26139
|
+
size,
|
|
26140
|
+
storageKey
|
|
26141
|
+
}
|
|
26142
|
+
});
|
|
26143
|
+
await notifySynced("ticketAttachment", att.id);
|
|
26144
|
+
}
|
|
26145
|
+
await recordNewTicket(ticket.id, input.projectId, input.projectName, input.category, input.title);
|
|
26146
|
+
void pruneOldTickets();
|
|
26147
|
+
return { ticket, key };
|
|
26148
|
+
}
|
|
26149
|
+
|
|
26097
26150
|
// src/services/help-ratelimit.ts
|
|
26098
26151
|
init_config2();
|
|
26099
26152
|
var ipBuckets = /* @__PURE__ */ new Map();
|
|
26100
26153
|
var projBuckets = /* @__PURE__ */ new Map();
|
|
26154
|
+
var acctBuckets = /* @__PURE__ */ new Map();
|
|
26101
26155
|
function take(map, k, cap, now) {
|
|
26102
26156
|
const b = map.get(k) ?? { tokens: cap, ts: now };
|
|
26103
26157
|
b.tokens = Math.min(cap, b.tokens + (now - b.ts) * cap / 6e4);
|
|
@@ -26115,13 +26169,15 @@ function helpRateOk(projectId, ip, now = Date.now()) {
|
|
|
26115
26169
|
const projCap = effectiveInt("HANOMAN_HELP_RATE_PER_MIN_PROJECT") ?? 20;
|
|
26116
26170
|
return take(ipBuckets, ip, ipCap, now) && take(projBuckets, projectId, projCap, now);
|
|
26117
26171
|
}
|
|
26172
|
+
function portalTicketRateOk(userId, projectId, now = Date.now()) {
|
|
26173
|
+
const acctCap = effectiveInt("HANOMAN_PORTAL_TICKET_RATE_PER_MIN") ?? 5;
|
|
26174
|
+
const projCap = effectiveInt("HANOMAN_HELP_RATE_PER_MIN_PROJECT") ?? 20;
|
|
26175
|
+
return take(acctBuckets, userId, acctCap, now) && take(projBuckets, projectId, projCap, now);
|
|
26176
|
+
}
|
|
26118
26177
|
|
|
26119
26178
|
// src/routes/help.ts
|
|
26120
26179
|
var CATEGORIES = ["bug", "fitur", "pertanyaan", "lainnya"];
|
|
26121
26180
|
var HONEYPOT_FIELD = "hc_trap";
|
|
26122
|
-
var OK_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
26123
|
-
var MAX_FILES2 = 3;
|
|
26124
|
-
var MAX_BYTES = 5 * 1024 * 1024;
|
|
26125
26181
|
var zField = external_exports.object({
|
|
26126
26182
|
category: zTicketCategory,
|
|
26127
26183
|
title: external_exports.string().min(1).max(200),
|
|
@@ -26141,21 +26197,9 @@ async function help_default(app2) {
|
|
|
26141
26197
|
if (!p || !p.helpEnabled) return reply.code(404).send({ error: "not found" });
|
|
26142
26198
|
if (!helpRateOk(slug, req.ip)) return reply.code(429).send({ error: "terlalu banyak permintaan" });
|
|
26143
26199
|
if (!req.isMultipart?.()) return reply.code(400).send({ error: "butuh multipart/form-data" });
|
|
26144
|
-
const
|
|
26145
|
-
|
|
26146
|
-
|
|
26147
|
-
for await (const part of req.parts()) {
|
|
26148
|
-
if (part.type === "file") {
|
|
26149
|
-
const buf = await part.toBuffer();
|
|
26150
|
-
if (part.file?.truncated || !OK_MIME.has(part.mimetype) || files.length >= MAX_FILES2) continue;
|
|
26151
|
-
files.push({ buf, mime: part.mimetype, name: String(part.filename ?? "gambar") });
|
|
26152
|
-
} else {
|
|
26153
|
-
fields[part.fieldname] = String(part.value ?? "");
|
|
26154
|
-
}
|
|
26155
|
-
}
|
|
26156
|
-
} catch {
|
|
26157
|
-
return reply.code(400).send({ error: "unggahan tak valid" });
|
|
26158
|
-
}
|
|
26200
|
+
const upload = await parseTicketUpload(req);
|
|
26201
|
+
if (!upload) return reply.code(400).send({ error: "unggahan tak valid" });
|
|
26202
|
+
const { fields, files } = upload;
|
|
26159
26203
|
if (fields[HONEYPOT_FIELD]) {
|
|
26160
26204
|
console.warn(`help: honeypot terpicu \xB7 project=${slug} ip=${req.ip}`);
|
|
26161
26205
|
return reply.code(200).send({ ok: true });
|
|
@@ -26167,23 +26211,15 @@ async function help_default(app2) {
|
|
|
26167
26211
|
email: fields.email
|
|
26168
26212
|
});
|
|
26169
26213
|
if (!parsed.success) return reply.code(400).send({ error: "field wajib tak lengkap / tak valid" });
|
|
26170
|
-
const { ticket, key } = await
|
|
26214
|
+
const { ticket, key } = await intakeTicket({
|
|
26171
26215
|
projectId: slug,
|
|
26216
|
+
projectName: p.name,
|
|
26172
26217
|
category: parsed.data.category,
|
|
26173
26218
|
title: parsed.data.title,
|
|
26174
26219
|
detail: parsed.data.detail,
|
|
26175
|
-
reporterEmail: parsed.data.email
|
|
26220
|
+
reporterEmail: parsed.data.email,
|
|
26221
|
+
files
|
|
26176
26222
|
});
|
|
26177
|
-
await notifySynced("ticket", ticket.id);
|
|
26178
|
-
for (const f of files) {
|
|
26179
|
-
const { storageKey, size } = await saveUpload(f.buf, f.mime);
|
|
26180
|
-
const att = await prisma.ticketAttachment.create({
|
|
26181
|
-
data: { ticketId: ticket.id, projectId: slug, filename: f.name.slice(0, 200), mimeType: f.mime, size, storageKey }
|
|
26182
|
-
});
|
|
26183
|
-
await notifySynced("ticketAttachment", att.id);
|
|
26184
|
-
}
|
|
26185
|
-
await recordNewTicket(ticket.id, slug, p.name, parsed.data.category, parsed.data.title);
|
|
26186
|
-
void pruneOldTickets();
|
|
26187
26223
|
const statusPath = `/help/${encodeURIComponent(slug)}/status/${encodeURIComponent(key)}`;
|
|
26188
26224
|
return reply.code(201).send({ number: ticket.number, key, statusPath });
|
|
26189
26225
|
});
|
|
@@ -30038,18 +30074,20 @@ async function webhooks_default(app2) {
|
|
|
30038
30074
|
}
|
|
30039
30075
|
|
|
30040
30076
|
// src/routes/portal.ts
|
|
30077
|
+
init_zod();
|
|
30041
30078
|
init_src();
|
|
30042
30079
|
init_db();
|
|
30043
30080
|
|
|
30044
30081
|
// src/services/client-access.ts
|
|
30045
30082
|
init_db();
|
|
30046
30083
|
var segments = (path) => path.replace(/^\/api\/?/, "").replace(/\/+$/, "").split("/").filter(Boolean);
|
|
30084
|
+
var isPortalTicketSubmit = (method, seg) => method === "POST" && seg.length === 4 && seg[1] === "projects" && seg[3] === "tickets";
|
|
30047
30085
|
function clientRouteAllowed(method, path) {
|
|
30048
30086
|
const seg = segments(path);
|
|
30049
30087
|
if (seg.includes("..")) return false;
|
|
30050
30088
|
const top = seg[0] ?? "";
|
|
30051
30089
|
const read = method === "GET" || method === "HEAD";
|
|
30052
|
-
if (top === "portal") return read;
|
|
30090
|
+
if (top === "portal") return read || isPortalTicketSubmit(method, seg);
|
|
30053
30091
|
if (top === "help") return true;
|
|
30054
30092
|
if (top === "auth") {
|
|
30055
30093
|
const sub = seg[1] ?? "";
|
|
@@ -30071,6 +30109,11 @@ async function hasProjectAccess(userId, projectId) {
|
|
|
30071
30109
|
|
|
30072
30110
|
// src/routes/portal.ts
|
|
30073
30111
|
var NOT_FOUND = { error: "not found" };
|
|
30112
|
+
var zPortalTicket = external_exports.object({
|
|
30113
|
+
category: zTicketCategory,
|
|
30114
|
+
title: external_exports.string().min(1).max(200),
|
|
30115
|
+
detail: external_exports.string().min(1).max(1e4)
|
|
30116
|
+
});
|
|
30074
30117
|
async function specStages(ids) {
|
|
30075
30118
|
const wanted = [...new Set(ids.filter((v) => !!v))];
|
|
30076
30119
|
if (!wanted.length) return /* @__PURE__ */ new Map();
|
|
@@ -30113,6 +30156,31 @@ async function portal_default(app2) {
|
|
|
30113
30156
|
const stages = await specStages([t.specId]);
|
|
30114
30157
|
return toPortalTicketDetail(t, stages.get(t.specId ?? "") ?? null);
|
|
30115
30158
|
});
|
|
30159
|
+
app2.post("/portal/projects/:id/tickets", async (req, reply) => {
|
|
30160
|
+
const { id } = req.params;
|
|
30161
|
+
const p = await prisma.project.findUnique({ where: { id } });
|
|
30162
|
+
if (!p || !await hasProjectAccess(req.user.id, id)) return reply.code(404).send(NOT_FOUND);
|
|
30163
|
+
if (!portalTicketRateOk(req.user.id, id)) return reply.code(429).send({ error: "terlalu banyak permintaan" });
|
|
30164
|
+
if (!req.isMultipart?.()) return reply.code(400).send({ error: "butuh multipart/form-data" });
|
|
30165
|
+
const upload = await parseTicketUpload(req);
|
|
30166
|
+
if (!upload) return reply.code(400).send({ error: "unggahan tak valid" });
|
|
30167
|
+
const parsed = zPortalTicket.safeParse({
|
|
30168
|
+
category: upload.fields.category,
|
|
30169
|
+
title: upload.fields.title,
|
|
30170
|
+
detail: upload.fields.detail
|
|
30171
|
+
});
|
|
30172
|
+
if (!parsed.success) return reply.code(400).send({ error: "field wajib tak lengkap / tak valid" });
|
|
30173
|
+
const { ticket } = await intakeTicket({
|
|
30174
|
+
projectId: id,
|
|
30175
|
+
projectName: p.name,
|
|
30176
|
+
category: parsed.data.category,
|
|
30177
|
+
title: parsed.data.title,
|
|
30178
|
+
detail: parsed.data.detail,
|
|
30179
|
+
reporterEmail: req.user.email,
|
|
30180
|
+
files: upload.files
|
|
30181
|
+
});
|
|
30182
|
+
return reply.code(201).send(toPortalTicket(ticket, null));
|
|
30183
|
+
});
|
|
30116
30184
|
}
|
|
30117
30185
|
|
|
30118
30186
|
// src/routes/client-accounts.ts
|