hanoman 0.1.23 → 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/cli.js +24 -1
- package/dist/server.js +337 -53
- package/docs/agent-integration.md +4 -0
- package/package.json +1 -1
- package/prisma/migrations/20260810000000_client_portal_access/migration.sql +15 -0
- package/prisma/schema.prisma +25 -1
- package/web/assets/{index-DAlC1JOD.js → index-D68Z05gR.js} +1543 -1543
- package/web/index.html +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -4986,7 +4986,7 @@ var init_prd_status = __esm({
|
|
|
4986
4986
|
});
|
|
4987
4987
|
|
|
4988
4988
|
// ../shared/src/dto.ts
|
|
4989
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
4989
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
4990
4990
|
var init_dto = __esm({
|
|
4991
4991
|
"../shared/src/dto.ts"() {
|
|
4992
4992
|
"use strict";
|
|
@@ -5400,12 +5400,24 @@ var init_dto = __esm({
|
|
|
5400
5400
|
password: external_exports.string().min(1)
|
|
5401
5401
|
// SPEC-165 · diisi = bootstrap ulang
|
|
5402
5402
|
}).partial();
|
|
5403
|
+
USER_ROLES = ["admin", "client"];
|
|
5404
|
+
zUserRole = external_exports.enum(USER_ROLES);
|
|
5403
5405
|
zLogin = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(1) });
|
|
5404
5406
|
zSignup = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(8) });
|
|
5405
5407
|
zChangePassword = external_exports.object({
|
|
5406
5408
|
currentPassword: external_exports.string().min(1),
|
|
5407
5409
|
newPassword: external_exports.string().min(8)
|
|
5408
5410
|
});
|
|
5411
|
+
zCreateClientAccount = external_exports.object({
|
|
5412
|
+
email: external_exports.string().email(),
|
|
5413
|
+
password: external_exports.string().min(8),
|
|
5414
|
+
projects: external_exports.array(external_exports.string().min(1)).default([])
|
|
5415
|
+
});
|
|
5416
|
+
zUpdateClientAccount = external_exports.object({
|
|
5417
|
+
projects: external_exports.array(external_exports.string().min(1)),
|
|
5418
|
+
disabled: external_exports.boolean(),
|
|
5419
|
+
password: external_exports.string().min(8)
|
|
5420
|
+
}).partial();
|
|
5409
5421
|
zVpsCheck = external_exports.object({
|
|
5410
5422
|
check: external_exports.string(),
|
|
5411
5423
|
status: external_exports.enum(["pass", "fail", "warn", "na"]),
|
|
@@ -5741,6 +5753,14 @@ var init_ticket_status = __esm({
|
|
|
5741
5753
|
}
|
|
5742
5754
|
});
|
|
5743
5755
|
|
|
5756
|
+
// ../shared/src/portal.ts
|
|
5757
|
+
var init_portal = __esm({
|
|
5758
|
+
"../shared/src/portal.ts"() {
|
|
5759
|
+
"use strict";
|
|
5760
|
+
init_ticket_status();
|
|
5761
|
+
}
|
|
5762
|
+
});
|
|
5763
|
+
|
|
5744
5764
|
// ../shared/src/session-kind.ts
|
|
5745
5765
|
var SESSION_KINDS, zSessionKind;
|
|
5746
5766
|
var init_session_kind = __esm({
|
|
@@ -7022,6 +7042,7 @@ var init_src = __esm({
|
|
|
7022
7042
|
init_api();
|
|
7023
7043
|
init_coverage();
|
|
7024
7044
|
init_ticket_status();
|
|
7045
|
+
init_portal();
|
|
7025
7046
|
init_prd_status();
|
|
7026
7047
|
init_session_kind();
|
|
7027
7048
|
init_config();
|
|
@@ -28591,7 +28612,9 @@ var init_migrate_pg = __esm({
|
|
|
28591
28612
|
// SPEC-516 · ADR-0105 · Changelog sesudah Project (FK projectId). Tabel ini LOCAL-only dan
|
|
28592
28613
|
// lazimnya TIDAK ada di sumber Postgres lama — jalur 42P01 memperlakukannya sebagai nol baris.
|
|
28593
28614
|
"Changelog",
|
|
28615
|
+
// SPEC-617 · ADR-0110 · ClientProjectAccess sesudah User DAN Project (FK ke keduanya).
|
|
28594
28616
|
"User",
|
|
28617
|
+
"ClientProjectAccess",
|
|
28595
28618
|
"Session",
|
|
28596
28619
|
"DeviceToken",
|
|
28597
28620
|
"AgentToken",
|
package/dist/server.js
CHANGED
|
@@ -5237,7 +5237,7 @@ var init_prd_status = __esm({
|
|
|
5237
5237
|
function flowForSource(source) {
|
|
5238
5238
|
return source === "qa" ? "qa" : source === "audit" ? "audit" : source === "goal" ? "goal" : "feature";
|
|
5239
5239
|
}
|
|
5240
|
-
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, zLogin, zSignup, zChangePassword, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
5240
|
+
var zIssueDeviceToken, zSessionResult, zSessionHistory, zCreateProject, zProjectId, zRenameProject, zUpdateProject, zCreateSpec, zPatchSpec, zChangeSpecSource, zIntegrate, zSessionSummary, zProjectView, zSchedulerQueueItem, zSchedulerSourceView, zSchedulerSessionView, zSchedulerQueueCounts, zSchedulerState, zLeadDecisionView, zLeadFlowView, zLeadAnswer, zLeadProjectStatus, zLeadStatusView, zFlow, zPrdBrief, zPrdDoc, zBreakdownItem, zBreakdownDoc, zBatchCreateSpec, zEscalationTarget, zEscalationPrefill, zAuditEscalation, zAuditEscalationView, zTerminalSession, zDocFileContent, zDocIndexCat, zDocIndex, HOST_RE, USER_RE, zCreateVps, zPatchVps, USER_ROLES, zUserRole, zLogin, zSignup, zChangePassword, zCreateClientAccount, zUpdateClientAccount, zVpsCheck, zMarkNa, zMarkNaBulk, zAttest, zRemediate, UPDATE_RESTART_EXIT, zUpdateApplyBody, zTicketView, zTicketAttachmentView, zTicketDetail, zTicketEditInput, zHelpInfo, zPublicTicketStatus;
|
|
5241
5241
|
var init_dto = __esm({
|
|
5242
5242
|
"../shared/src/dto.ts"() {
|
|
5243
5243
|
"use strict";
|
|
@@ -5651,12 +5651,24 @@ var init_dto = __esm({
|
|
|
5651
5651
|
password: external_exports.string().min(1)
|
|
5652
5652
|
// SPEC-165 · diisi = bootstrap ulang
|
|
5653
5653
|
}).partial();
|
|
5654
|
+
USER_ROLES = ["admin", "client"];
|
|
5655
|
+
zUserRole = external_exports.enum(USER_ROLES);
|
|
5654
5656
|
zLogin = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(1) });
|
|
5655
5657
|
zSignup = external_exports.object({ email: external_exports.string().email(), password: external_exports.string().min(8) });
|
|
5656
5658
|
zChangePassword = external_exports.object({
|
|
5657
5659
|
currentPassword: external_exports.string().min(1),
|
|
5658
5660
|
newPassword: external_exports.string().min(8)
|
|
5659
5661
|
});
|
|
5662
|
+
zCreateClientAccount = external_exports.object({
|
|
5663
|
+
email: external_exports.string().email(),
|
|
5664
|
+
password: external_exports.string().min(8),
|
|
5665
|
+
projects: external_exports.array(external_exports.string().min(1)).default([])
|
|
5666
|
+
});
|
|
5667
|
+
zUpdateClientAccount = external_exports.object({
|
|
5668
|
+
projects: external_exports.array(external_exports.string().min(1)),
|
|
5669
|
+
disabled: external_exports.boolean(),
|
|
5670
|
+
password: external_exports.string().min(8)
|
|
5671
|
+
}).partial();
|
|
5660
5672
|
zVpsCheck = external_exports.object({
|
|
5661
5673
|
check: external_exports.string(),
|
|
5662
5674
|
status: external_exports.enum(["pass", "fail", "warn", "na"]),
|
|
@@ -6002,6 +6014,44 @@ var init_ticket_status = __esm({
|
|
|
6002
6014
|
}
|
|
6003
6015
|
});
|
|
6004
6016
|
|
|
6017
|
+
// ../shared/src/portal.ts
|
|
6018
|
+
function toPortalProject(p) {
|
|
6019
|
+
return { id: p.id, name: p.name };
|
|
6020
|
+
}
|
|
6021
|
+
function toPortalSpec(s2) {
|
|
6022
|
+
return {
|
|
6023
|
+
id: s2.id,
|
|
6024
|
+
title: s2.title,
|
|
6025
|
+
priority: s2.priority,
|
|
6026
|
+
stage: s2.stage,
|
|
6027
|
+
objective: s2.objective,
|
|
6028
|
+
createdAt: iso(s2.createdAt),
|
|
6029
|
+
startedAt: iso(s2.startedAt),
|
|
6030
|
+
doneAt: iso(s2.doneAt)
|
|
6031
|
+
};
|
|
6032
|
+
}
|
|
6033
|
+
function toPortalTicket(t, specStage) {
|
|
6034
|
+
return {
|
|
6035
|
+
id: t.id,
|
|
6036
|
+
number: t.number,
|
|
6037
|
+
category: t.category,
|
|
6038
|
+
title: t.title,
|
|
6039
|
+
status: publicStatus(t.status, specStage),
|
|
6040
|
+
createdAt: iso(t.createdAt)
|
|
6041
|
+
};
|
|
6042
|
+
}
|
|
6043
|
+
function toPortalTicketDetail(t, specStage) {
|
|
6044
|
+
return { ...toPortalTicket(t, specStage), detail: t.detail };
|
|
6045
|
+
}
|
|
6046
|
+
var iso;
|
|
6047
|
+
var init_portal = __esm({
|
|
6048
|
+
"../shared/src/portal.ts"() {
|
|
6049
|
+
"use strict";
|
|
6050
|
+
init_ticket_status();
|
|
6051
|
+
iso = (d) => d == null ? null : typeof d === "string" ? d : d.toISOString();
|
|
6052
|
+
}
|
|
6053
|
+
});
|
|
6054
|
+
|
|
6005
6055
|
// ../shared/src/session-kind.ts
|
|
6006
6056
|
var SESSION_KINDS, zSessionKind;
|
|
6007
6057
|
var init_session_kind = __esm({
|
|
@@ -6315,14 +6365,14 @@ function matchesEvent(subscribed, type) {
|
|
|
6315
6365
|
}
|
|
6316
6366
|
function projectRow(def2, row) {
|
|
6317
6367
|
const out3 = {};
|
|
6318
|
-
for (const f of def2.fields) if (f in row) out3[f] =
|
|
6368
|
+
for (const f of def2.fields) if (f in row) out3[f] = iso2(row[f]);
|
|
6319
6369
|
return out3;
|
|
6320
6370
|
}
|
|
6321
6371
|
function diffFields(def2, before, after) {
|
|
6322
6372
|
const out3 = [];
|
|
6323
6373
|
for (const f of def2.fields) {
|
|
6324
6374
|
if (IGNORED_FIELDS.has(f)) continue;
|
|
6325
|
-
if (JSON.stringify(
|
|
6375
|
+
if (JSON.stringify(iso2(before[f]) ?? null) !== JSON.stringify(iso2(after[f]) ?? null)) out3.push(f);
|
|
6326
6376
|
}
|
|
6327
6377
|
return out3;
|
|
6328
6378
|
}
|
|
@@ -6361,7 +6411,7 @@ function clampEnvelope(env) {
|
|
|
6361
6411
|
}
|
|
6362
6412
|
return { ...env, truncated: true, truncatedFields: fields };
|
|
6363
6413
|
}
|
|
6364
|
-
var WEBHOOK_SPEC_VERSION, WEBHOOK_BACKOFF_SEC, WEBHOOK_MAX_ATTEMPTS, WEBHOOK_FAIL_LIMIT, WEBHOOK_QUEUE_CAP, WEBHOOK_HISTORY_KEEP, WEBHOOK_MAX_BYTES, WEBHOOK_FIELD_MAX_CHARS, WEBHOOK_TIMEOUT_MS, WEBHOOK_DEFAULT_PER_MINUTE, WEBHOOK_PING_TYPE, WEBHOOK_USER_AGENT, WEBHOOK_HEADERS, SYSTEM_ACTOR, IGNORED_FIELDS, WEBHOOK_ENTITIES, BY_MODEL, WEBHOOK_EVENTS, webhookEventTypes,
|
|
6414
|
+
var WEBHOOK_SPEC_VERSION, WEBHOOK_BACKOFF_SEC, WEBHOOK_MAX_ATTEMPTS, WEBHOOK_FAIL_LIMIT, WEBHOOK_QUEUE_CAP, WEBHOOK_HISTORY_KEEP, WEBHOOK_MAX_BYTES, WEBHOOK_FIELD_MAX_CHARS, WEBHOOK_TIMEOUT_MS, WEBHOOK_DEFAULT_PER_MINUTE, WEBHOOK_PING_TYPE, WEBHOOK_USER_AGENT, WEBHOOK_HEADERS, SYSTEM_ACTOR, IGNORED_FIELDS, WEBHOOK_ENTITIES, BY_MODEL, WEBHOOK_EVENTS, webhookEventTypes, iso2, zCreateWebhookEndpoint, zUpdateWebhookEndpoint;
|
|
6365
6415
|
var init_webhook = __esm({
|
|
6366
6416
|
"../shared/src/webhook.ts"() {
|
|
6367
6417
|
"use strict";
|
|
@@ -6718,7 +6768,7 @@ var init_webhook = __esm({
|
|
|
6718
6768
|
}
|
|
6719
6769
|
];
|
|
6720
6770
|
webhookEventTypes = () => WEBHOOK_EVENTS.map((e) => e.type);
|
|
6721
|
-
|
|
6771
|
+
iso2 = (v) => v instanceof Date ? v.toISOString() : v;
|
|
6722
6772
|
zCreateWebhookEndpoint = external_exports.object({
|
|
6723
6773
|
name: external_exports.string().trim().min(1).max(80),
|
|
6724
6774
|
url: external_exports.string().trim().min(1).max(2e3),
|
|
@@ -7381,6 +7431,7 @@ var init_src = __esm({
|
|
|
7381
7431
|
init_api();
|
|
7382
7432
|
init_coverage();
|
|
7383
7433
|
init_ticket_status();
|
|
7434
|
+
init_portal();
|
|
7384
7435
|
init_prd_status();
|
|
7385
7436
|
init_session_kind();
|
|
7386
7437
|
init_config();
|
|
@@ -26043,10 +26094,64 @@ async function pruneOldTickets(now = Date.now()) {
|
|
|
26043
26094
|
}
|
|
26044
26095
|
}
|
|
26045
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
|
+
|
|
26046
26150
|
// src/services/help-ratelimit.ts
|
|
26047
26151
|
init_config2();
|
|
26048
26152
|
var ipBuckets = /* @__PURE__ */ new Map();
|
|
26049
26153
|
var projBuckets = /* @__PURE__ */ new Map();
|
|
26154
|
+
var acctBuckets = /* @__PURE__ */ new Map();
|
|
26050
26155
|
function take(map, k, cap, now) {
|
|
26051
26156
|
const b = map.get(k) ?? { tokens: cap, ts: now };
|
|
26052
26157
|
b.tokens = Math.min(cap, b.tokens + (now - b.ts) * cap / 6e4);
|
|
@@ -26064,13 +26169,15 @@ function helpRateOk(projectId, ip, now = Date.now()) {
|
|
|
26064
26169
|
const projCap = effectiveInt("HANOMAN_HELP_RATE_PER_MIN_PROJECT") ?? 20;
|
|
26065
26170
|
return take(ipBuckets, ip, ipCap, now) && take(projBuckets, projectId, projCap, now);
|
|
26066
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
|
+
}
|
|
26067
26177
|
|
|
26068
26178
|
// src/routes/help.ts
|
|
26069
26179
|
var CATEGORIES = ["bug", "fitur", "pertanyaan", "lainnya"];
|
|
26070
26180
|
var HONEYPOT_FIELD = "hc_trap";
|
|
26071
|
-
var OK_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
26072
|
-
var MAX_FILES2 = 3;
|
|
26073
|
-
var MAX_BYTES = 5 * 1024 * 1024;
|
|
26074
26181
|
var zField = external_exports.object({
|
|
26075
26182
|
category: zTicketCategory,
|
|
26076
26183
|
title: external_exports.string().min(1).max(200),
|
|
@@ -26090,21 +26197,9 @@ async function help_default(app2) {
|
|
|
26090
26197
|
if (!p || !p.helpEnabled) return reply.code(404).send({ error: "not found" });
|
|
26091
26198
|
if (!helpRateOk(slug, req.ip)) return reply.code(429).send({ error: "terlalu banyak permintaan" });
|
|
26092
26199
|
if (!req.isMultipart?.()) return reply.code(400).send({ error: "butuh multipart/form-data" });
|
|
26093
|
-
const
|
|
26094
|
-
|
|
26095
|
-
|
|
26096
|
-
for await (const part of req.parts()) {
|
|
26097
|
-
if (part.type === "file") {
|
|
26098
|
-
const buf = await part.toBuffer();
|
|
26099
|
-
if (part.file?.truncated || !OK_MIME.has(part.mimetype) || files.length >= MAX_FILES2) continue;
|
|
26100
|
-
files.push({ buf, mime: part.mimetype, name: String(part.filename ?? "gambar") });
|
|
26101
|
-
} else {
|
|
26102
|
-
fields[part.fieldname] = String(part.value ?? "");
|
|
26103
|
-
}
|
|
26104
|
-
}
|
|
26105
|
-
} catch {
|
|
26106
|
-
return reply.code(400).send({ error: "unggahan tak valid" });
|
|
26107
|
-
}
|
|
26200
|
+
const upload = await parseTicketUpload(req);
|
|
26201
|
+
if (!upload) return reply.code(400).send({ error: "unggahan tak valid" });
|
|
26202
|
+
const { fields, files } = upload;
|
|
26108
26203
|
if (fields[HONEYPOT_FIELD]) {
|
|
26109
26204
|
console.warn(`help: honeypot terpicu \xB7 project=${slug} ip=${req.ip}`);
|
|
26110
26205
|
return reply.code(200).send({ ok: true });
|
|
@@ -26116,23 +26211,15 @@ async function help_default(app2) {
|
|
|
26116
26211
|
email: fields.email
|
|
26117
26212
|
});
|
|
26118
26213
|
if (!parsed.success) return reply.code(400).send({ error: "field wajib tak lengkap / tak valid" });
|
|
26119
|
-
const { ticket, key } = await
|
|
26214
|
+
const { ticket, key } = await intakeTicket({
|
|
26120
26215
|
projectId: slug,
|
|
26216
|
+
projectName: p.name,
|
|
26121
26217
|
category: parsed.data.category,
|
|
26122
26218
|
title: parsed.data.title,
|
|
26123
26219
|
detail: parsed.data.detail,
|
|
26124
|
-
reporterEmail: parsed.data.email
|
|
26220
|
+
reporterEmail: parsed.data.email,
|
|
26221
|
+
files
|
|
26125
26222
|
});
|
|
26126
|
-
await notifySynced("ticket", ticket.id);
|
|
26127
|
-
for (const f of files) {
|
|
26128
|
-
const { storageKey, size } = await saveUpload(f.buf, f.mime);
|
|
26129
|
-
const att = await prisma.ticketAttachment.create({
|
|
26130
|
-
data: { ticketId: ticket.id, projectId: slug, filename: f.name.slice(0, 200), mimeType: f.mime, size, storageKey }
|
|
26131
|
-
});
|
|
26132
|
-
await notifySynced("ticketAttachment", att.id);
|
|
26133
|
-
}
|
|
26134
|
-
await recordNewTicket(ticket.id, slug, p.name, parsed.data.category, parsed.data.title);
|
|
26135
|
-
void pruneOldTickets();
|
|
26136
26223
|
const statusPath = `/help/${encodeURIComponent(slug)}/status/${encodeURIComponent(key)}`;
|
|
26137
26224
|
return reply.code(201).send({ number: ticket.number, key, statusPath });
|
|
26138
26225
|
});
|
|
@@ -29986,10 +30073,117 @@ async function webhooks_default(app2) {
|
|
|
29986
30073
|
});
|
|
29987
30074
|
}
|
|
29988
30075
|
|
|
29989
|
-
// src/
|
|
29990
|
-
|
|
30076
|
+
// src/routes/portal.ts
|
|
30077
|
+
init_zod();
|
|
30078
|
+
init_src();
|
|
30079
|
+
init_db();
|
|
29991
30080
|
|
|
29992
|
-
// src/
|
|
30081
|
+
// src/services/client-access.ts
|
|
30082
|
+
init_db();
|
|
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";
|
|
30085
|
+
function clientRouteAllowed(method, path) {
|
|
30086
|
+
const seg = segments(path);
|
|
30087
|
+
if (seg.includes("..")) return false;
|
|
30088
|
+
const top = seg[0] ?? "";
|
|
30089
|
+
const read = method === "GET" || method === "HEAD";
|
|
30090
|
+
if (top === "portal") return read || isPortalTicketSubmit(method, seg);
|
|
30091
|
+
if (top === "help") return true;
|
|
30092
|
+
if (top === "auth") {
|
|
30093
|
+
const sub = seg[1] ?? "";
|
|
30094
|
+
return method === "POST" && (sub === "logout" || sub === "change-password");
|
|
30095
|
+
}
|
|
30096
|
+
return false;
|
|
30097
|
+
}
|
|
30098
|
+
async function clientProjectIds(userId) {
|
|
30099
|
+
const rows = await prisma.clientProjectAccess.findMany({
|
|
30100
|
+
where: { userId },
|
|
30101
|
+
select: { projectId: true },
|
|
30102
|
+
orderBy: { projectId: "asc" }
|
|
30103
|
+
});
|
|
30104
|
+
return rows.map((r) => r.projectId);
|
|
30105
|
+
}
|
|
30106
|
+
async function hasProjectAccess(userId, projectId) {
|
|
30107
|
+
return await prisma.clientProjectAccess.count({ where: { userId, projectId } }) > 0;
|
|
30108
|
+
}
|
|
30109
|
+
|
|
30110
|
+
// src/routes/portal.ts
|
|
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
|
+
});
|
|
30117
|
+
async function specStages(ids) {
|
|
30118
|
+
const wanted = [...new Set(ids.filter((v) => !!v))];
|
|
30119
|
+
if (!wanted.length) return /* @__PURE__ */ new Map();
|
|
30120
|
+
const rows = await prisma.spec.findMany({ where: { id: { in: wanted } }, select: { id: true, stage: true } });
|
|
30121
|
+
return new Map(rows.map((r) => [r.id, r.stage]));
|
|
30122
|
+
}
|
|
30123
|
+
async function portal_default(app2) {
|
|
30124
|
+
app2.get("/portal/projects", async (req) => {
|
|
30125
|
+
const ids = await clientProjectIds(req.user.id);
|
|
30126
|
+
const rows = await prisma.project.findMany({ where: { id: { in: ids } }, orderBy: { name: "asc" } });
|
|
30127
|
+
return { items: rows.map(toPortalProject) };
|
|
30128
|
+
});
|
|
30129
|
+
app2.get("/portal/projects/:id/backlog", async (req, reply) => {
|
|
30130
|
+
const { id } = req.params;
|
|
30131
|
+
if (!await hasProjectAccess(req.user.id, id)) return reply.code(404).send(NOT_FOUND);
|
|
30132
|
+
const { page, limit } = req.query;
|
|
30133
|
+
const specs = await liveSpecs({ project: id });
|
|
30134
|
+
return paginate(specs.map(toPortalSpec), page, limit);
|
|
30135
|
+
});
|
|
30136
|
+
app2.get("/portal/projects/:id/backlog/:specId", async (req, reply) => {
|
|
30137
|
+
const { id, specId } = req.params;
|
|
30138
|
+
if (!await hasProjectAccess(req.user.id, id)) return reply.code(404).send(NOT_FOUND);
|
|
30139
|
+
const spec = (await liveSpecs({ project: id })).find((s2) => s2.id === specId);
|
|
30140
|
+
if (!spec) return reply.code(404).send(NOT_FOUND);
|
|
30141
|
+
return toPortalSpec(spec);
|
|
30142
|
+
});
|
|
30143
|
+
app2.get("/portal/projects/:id/tickets", async (req, reply) => {
|
|
30144
|
+
const { id } = req.params;
|
|
30145
|
+
if (!await hasProjectAccess(req.user.id, id)) return reply.code(404).send(NOT_FOUND);
|
|
30146
|
+
const { page, limit } = req.query;
|
|
30147
|
+
const rows = await prisma.ticket.findMany({ where: { projectId: id }, orderBy: { createdAt: "desc" } });
|
|
30148
|
+
const stages = await specStages(rows.map((t) => t.specId));
|
|
30149
|
+
return paginate(rows.map((t) => toPortalTicket(t, stages.get(t.specId ?? "") ?? null)), page, limit);
|
|
30150
|
+
});
|
|
30151
|
+
app2.get("/portal/projects/:id/tickets/:ticketId", async (req, reply) => {
|
|
30152
|
+
const { id, ticketId } = req.params;
|
|
30153
|
+
if (!await hasProjectAccess(req.user.id, id)) return reply.code(404).send(NOT_FOUND);
|
|
30154
|
+
const t = await prisma.ticket.findUnique({ where: { id: ticketId } });
|
|
30155
|
+
if (!t || t.projectId !== id) return reply.code(404).send(NOT_FOUND);
|
|
30156
|
+
const stages = await specStages([t.specId]);
|
|
30157
|
+
return toPortalTicketDetail(t, stages.get(t.specId ?? "") ?? null);
|
|
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
|
+
});
|
|
30184
|
+
}
|
|
30185
|
+
|
|
30186
|
+
// src/routes/client-accounts.ts
|
|
29993
30187
|
init_src();
|
|
29994
30188
|
init_db();
|
|
29995
30189
|
|
|
@@ -30029,7 +30223,13 @@ async function lookupSession(token) {
|
|
|
30029
30223
|
});
|
|
30030
30224
|
return null;
|
|
30031
30225
|
}
|
|
30032
|
-
|
|
30226
|
+
if (s2.user.disabled) return null;
|
|
30227
|
+
return {
|
|
30228
|
+
id: s2.user.id,
|
|
30229
|
+
email: s2.user.email,
|
|
30230
|
+
role: s2.user.role,
|
|
30231
|
+
createdAt: s2.user.createdAt.toISOString()
|
|
30232
|
+
};
|
|
30033
30233
|
}
|
|
30034
30234
|
async function deleteSession(token) {
|
|
30035
30235
|
await prisma.session.deleteMany({ where: { id: sessionId(token) } });
|
|
@@ -30066,8 +30266,83 @@ function cookieOpts() {
|
|
|
30066
30266
|
};
|
|
30067
30267
|
}
|
|
30068
30268
|
|
|
30269
|
+
// src/routes/client-accounts.ts
|
|
30270
|
+
var view9 = (u) => ({
|
|
30271
|
+
id: u.id,
|
|
30272
|
+
email: u.email,
|
|
30273
|
+
disabled: u.disabled,
|
|
30274
|
+
createdAt: u.createdAt.toISOString(),
|
|
30275
|
+
projects: u.projectAccess.map((a) => a.projectId).sort()
|
|
30276
|
+
});
|
|
30277
|
+
var load = (id) => prisma.user.findFirst({
|
|
30278
|
+
where: { id, role: "client" },
|
|
30279
|
+
include: { projectAccess: { select: { projectId: true } } }
|
|
30280
|
+
});
|
|
30281
|
+
async function setAccess(userId, projects) {
|
|
30282
|
+
const ids = [...new Set(projects)];
|
|
30283
|
+
const known = await prisma.project.findMany({ where: { id: { in: ids } }, select: { id: true } });
|
|
30284
|
+
if (known.length !== ids.length) return null;
|
|
30285
|
+
await prisma.clientProjectAccess.deleteMany({ where: { userId, projectId: { notIn: ids } } });
|
|
30286
|
+
for (const projectId of ids)
|
|
30287
|
+
await prisma.clientProjectAccess.upsert({
|
|
30288
|
+
where: { userId_projectId: { userId, projectId } },
|
|
30289
|
+
update: {},
|
|
30290
|
+
create: { userId, projectId }
|
|
30291
|
+
});
|
|
30292
|
+
return ids;
|
|
30293
|
+
}
|
|
30294
|
+
async function client_accounts_default(app2) {
|
|
30295
|
+
app2.get("/client-accounts", async () => {
|
|
30296
|
+
const rows = await prisma.user.findMany({
|
|
30297
|
+
where: { role: "client" },
|
|
30298
|
+
orderBy: { createdAt: "asc" },
|
|
30299
|
+
include: { projectAccess: { select: { projectId: true } } }
|
|
30300
|
+
});
|
|
30301
|
+
return { items: rows.map(view9) };
|
|
30302
|
+
});
|
|
30303
|
+
app2.post("/client-accounts", async (req, reply) => {
|
|
30304
|
+
const p = zCreateClientAccount.safeParse(req.body);
|
|
30305
|
+
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30306
|
+
if (await prisma.user.findUnique({ where: { email: p.data.email } }))
|
|
30307
|
+
return reply.code(409).send({ error: "email dipakai" });
|
|
30308
|
+
const user = await prisma.user.create({
|
|
30309
|
+
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password), role: "client" }
|
|
30310
|
+
});
|
|
30311
|
+
if (await setAccess(user.id, p.data.projects) === null) {
|
|
30312
|
+
await prisma.user.delete({ where: { id: user.id } });
|
|
30313
|
+
return reply.code(400).send({ error: "ada project yang tidak dikenal" });
|
|
30314
|
+
}
|
|
30315
|
+
return reply.code(201).send(view9(await load(user.id)));
|
|
30316
|
+
});
|
|
30317
|
+
app2.patch("/client-accounts/:id", async (req, reply) => {
|
|
30318
|
+
const { id } = req.params;
|
|
30319
|
+
const p = zUpdateClientAccount.safeParse(req.body);
|
|
30320
|
+
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30321
|
+
if (!await load(id)) return reply.code(404).send({ error: "not found" });
|
|
30322
|
+
if (p.data.projects && await setAccess(id, p.data.projects) === null)
|
|
30323
|
+
return reply.code(400).send({ error: "ada project yang tidak dikenal" });
|
|
30324
|
+
if (p.data.disabled !== void 0)
|
|
30325
|
+
await prisma.user.update({ where: { id }, data: { disabled: p.data.disabled } });
|
|
30326
|
+
if (p.data.password)
|
|
30327
|
+
await prisma.user.update({ where: { id }, data: { passwordHash: await hashPassword(p.data.password) } });
|
|
30328
|
+
if (p.data.disabled || p.data.password) await deleteUserSessions(id);
|
|
30329
|
+
return view9(await load(id));
|
|
30330
|
+
});
|
|
30331
|
+
app2.delete("/client-accounts/:id", async (req, reply) => {
|
|
30332
|
+
const { id } = req.params;
|
|
30333
|
+
if (!await load(id)) return reply.code(404).send({ error: "not found" });
|
|
30334
|
+
await prisma.user.delete({ where: { id } });
|
|
30335
|
+
return reply.code(204).send();
|
|
30336
|
+
});
|
|
30337
|
+
}
|
|
30338
|
+
|
|
30339
|
+
// src/app.ts
|
|
30340
|
+
var import_multipart = __toESM(require_multipart2(), 1);
|
|
30341
|
+
|
|
30069
30342
|
// src/routes/auth.ts
|
|
30070
|
-
|
|
30343
|
+
init_src();
|
|
30344
|
+
init_db();
|
|
30345
|
+
var view10 = (u) => ({ id: u.id, email: u.email, role: u.role, createdAt: u.createdAt.toISOString() });
|
|
30071
30346
|
async function issue(reply, userId) {
|
|
30072
30347
|
const token = await createSession2(userId);
|
|
30073
30348
|
reply.setCookie(COOKIE_NAME, token, cookieOpts());
|
|
@@ -30085,20 +30360,20 @@ async function auth_default(app2) {
|
|
|
30085
30360
|
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
|
|
30086
30361
|
});
|
|
30087
30362
|
await issue(reply, user.id);
|
|
30088
|
-
return { user:
|
|
30363
|
+
return { user: view10(user) };
|
|
30089
30364
|
});
|
|
30090
30365
|
app2.post("/auth/login", async (req, reply) => {
|
|
30091
30366
|
if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
|
|
30092
30367
|
const p = zLogin.safeParse(req.body);
|
|
30093
30368
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30094
30369
|
const user = await prisma.user.findUnique({ where: { email: p.data.email } });
|
|
30095
|
-
if (!user || !await verifyPassword(p.data.password, user.passwordHash)) {
|
|
30370
|
+
if (!user || user.disabled || !await verifyPassword(p.data.password, user.passwordHash)) {
|
|
30096
30371
|
noteLoginFail(req.ip);
|
|
30097
30372
|
return reply.code(401).send({ error: "email atau password salah" });
|
|
30098
30373
|
}
|
|
30099
30374
|
clearLoginFails(req.ip);
|
|
30100
30375
|
await issue(reply, user.id);
|
|
30101
|
-
return { user:
|
|
30376
|
+
return { user: view10(user) };
|
|
30102
30377
|
});
|
|
30103
30378
|
app2.post("/auth/logout", async (req, reply) => {
|
|
30104
30379
|
const token = req.cookies?.[COOKIE_NAME];
|
|
@@ -30106,19 +30381,24 @@ async function auth_default(app2) {
|
|
|
30106
30381
|
reply.clearCookie(COOKIE_NAME, { path: "/" });
|
|
30107
30382
|
return reply.code(204).send();
|
|
30108
30383
|
});
|
|
30109
|
-
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(
|
|
30384
|
+
app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view10));
|
|
30110
30385
|
app2.post("/auth/users", async (req, reply) => {
|
|
30111
30386
|
const p = zSignup.safeParse(req.body);
|
|
30112
30387
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30113
30388
|
if (await prisma.user.findUnique({ where: { email: p.data.email } }))
|
|
30114
30389
|
return reply.code(409).send({ error: "email dipakai" });
|
|
30115
30390
|
const user = await prisma.user.create({
|
|
30116
|
-
|
|
30391
|
+
// SPEC-617 · pintu ini melahirkan OPERATOR. Akun klien punya pintunya sendiri
|
|
30392
|
+
// (/api/client-accounts) supaya "undang rekan" dan "beri akses klien" tak pernah tertukar.
|
|
30393
|
+
data: { email: p.data.email, passwordHash: await hashPassword(p.data.password), role: "admin" }
|
|
30117
30394
|
});
|
|
30118
|
-
return
|
|
30395
|
+
return view10(user);
|
|
30119
30396
|
});
|
|
30120
30397
|
app2.delete("/auth/users/:id", async (req, reply) => {
|
|
30121
|
-
|
|
30398
|
+
const target2 = await prisma.user.findUnique({ where: { id: req.params.id } });
|
|
30399
|
+
if (!target2) return reply.code(204).send();
|
|
30400
|
+
if (target2.role === "admin" && await prisma.user.count({ where: { role: "admin" } }) <= 1)
|
|
30401
|
+
return reply.code(400).send({ error: "tak bisa hapus admin terakhir" });
|
|
30122
30402
|
await prisma.user.deleteMany({ where: { id: req.params.id } });
|
|
30123
30403
|
return reply.code(204).send();
|
|
30124
30404
|
});
|
|
@@ -30135,7 +30415,7 @@ async function auth_default(app2) {
|
|
|
30135
30415
|
});
|
|
30136
30416
|
await deleteUserSessions(user.id);
|
|
30137
30417
|
await issue(reply, user.id);
|
|
30138
|
-
return { user:
|
|
30418
|
+
return { user: view10(user) };
|
|
30139
30419
|
});
|
|
30140
30420
|
}
|
|
30141
30421
|
|
|
@@ -30148,15 +30428,15 @@ async function agent_tokens_default(app2) {
|
|
|
30148
30428
|
app2.post("/agent-tokens", async (req, reply) => {
|
|
30149
30429
|
const p = zAgentTokenCreate.safeParse(req.body);
|
|
30150
30430
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30151
|
-
const { view:
|
|
30152
|
-
return reply.code(201).send({ ...
|
|
30431
|
+
const { view: view11, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
|
|
30432
|
+
return reply.code(201).send({ ...view11, token });
|
|
30153
30433
|
});
|
|
30154
30434
|
app2.patch("/agent-tokens/:id", async (req, reply) => {
|
|
30155
30435
|
const p = zAgentTokenPatch.safeParse(req.body);
|
|
30156
30436
|
if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
|
|
30157
30437
|
const { id } = req.params;
|
|
30158
|
-
const
|
|
30159
|
-
return
|
|
30438
|
+
const view11 = await patchAgentToken(id, p.data);
|
|
30439
|
+
return view11 ? reply.send(view11) : reply.code(404).send({ error: "not found" });
|
|
30160
30440
|
});
|
|
30161
30441
|
app2.delete("/agent-tokens/:id", async (req, reply) => {
|
|
30162
30442
|
const { id } = req.params;
|
|
@@ -30201,7 +30481,7 @@ function capabilityForRoute(method, path) {
|
|
|
30201
30481
|
const rw = (d) => `${d}:${read ? "read" : "write"}`;
|
|
30202
30482
|
const seg = path.replace(/^\/api\/?/, "").replace(/\/+$/, "").split("/").filter(Boolean);
|
|
30203
30483
|
const top = seg[0] ?? "";
|
|
30204
|
-
if (top === "auth" || top === "agent-tokens" || top === "device-tokens" || top === "sync") return "COOKIE_ONLY";
|
|
30484
|
+
if (top === "auth" || top === "agent-tokens" || top === "device-tokens" || top === "sync" || top === "portal" || top === "client-accounts") return "COOKIE_ONLY";
|
|
30205
30485
|
if (top === "limits" || top === "update" || top === "events" || top === "fs" || top === "health")
|
|
30206
30486
|
return read ? "GLOBAL_READ" : "COOKIE_ONLY";
|
|
30207
30487
|
if (top === "scheduler") return rw("settings");
|
|
@@ -30400,6 +30680,8 @@ function buildApp({ requireAuth = true, agentDocFile } = {}) {
|
|
|
30400
30680
|
if (user) req.user = user;
|
|
30401
30681
|
const path = req.url.split("?")[0] ?? req.url;
|
|
30402
30682
|
if (PUBLIC.has(`${req.method} ${path}`)) return;
|
|
30683
|
+
if (user?.role === "client" && !clientRouteAllowed(req.method, path))
|
|
30684
|
+
return reply.code(403).send({ error: "portal klien: baca-saja" });
|
|
30403
30685
|
if (path.startsWith("/api/sync") && path !== "/api/sync/now" && !path.startsWith("/api/sync/conflicts")) return;
|
|
30404
30686
|
if (path.startsWith("/api/help")) return;
|
|
30405
30687
|
if (user) return;
|
|
@@ -30455,6 +30737,8 @@ function buildApp({ requireAuth = true, agentDocFile } = {}) {
|
|
|
30455
30737
|
await api.register(telegramRoutes);
|
|
30456
30738
|
await api.register(webhooks_default);
|
|
30457
30739
|
await api.register(changelog_default);
|
|
30740
|
+
await api.register(portal_default);
|
|
30741
|
+
await api.register(client_accounts_default);
|
|
30458
30742
|
}, { prefix: "/api" });
|
|
30459
30743
|
if (process.env.NODE_ENV === "production") {
|
|
30460
30744
|
const dist = pickWebDir(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync13);
|