@musnows/scriverse 0.3.9 → 0.3.10
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/app.js +163 -30
- package/dist/app.js.map +1 -1
- package/dist/public/app.js +213 -98
- package/dist/public/index.html +17 -4
- package/dist/public/styles.css +31 -22
- package/dist/public/work-permissions.d.ts +14 -0
- package/dist/public/work-permissions.js +71 -0
- package/dist/store.js +19 -7
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +200 -52
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/work-permissions.js +128 -0
- package/dist/work-permissions.js.map +1 -0
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -26,6 +26,7 @@ import { createRequestLoggingMiddleware, sanitizeRequestPath } from "./http-logg
|
|
|
26
26
|
import { accountReference, logger, sanitizeError } from "./logger.js";
|
|
27
27
|
import { runWithRequestActor } from "./request-context.js";
|
|
28
28
|
import { APP_VERSION } from "./version.js";
|
|
29
|
+
import { fullWorkModulePermissions } from "./work-permissions.js";
|
|
29
30
|
import { clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, setSessionCookie, UserAuthService } from "./user-auth.js";
|
|
30
31
|
const nonEmpty = z.string().trim().min(1);
|
|
31
32
|
const identifier = z.string().trim().min(1).max(200);
|
|
@@ -55,8 +56,29 @@ const loginSchema = z.object({
|
|
|
55
56
|
...captchaFields
|
|
56
57
|
}).strict();
|
|
57
58
|
const userUpdateSchema = z.object({ role: z.enum(["admin", "user"]).optional(), status: z.enum(["active", "disabled"]).optional() }).strict();
|
|
58
|
-
const
|
|
59
|
-
const
|
|
59
|
+
const memberRoleValueSchema = z.enum(["editor", "settings-editor", "viewer"]);
|
|
60
|
+
const moduleAccessSchema = z.enum(["none", "read", "write"]);
|
|
61
|
+
const modulePermissionsSchema = z.object({
|
|
62
|
+
prose: moduleAccessSchema,
|
|
63
|
+
settings: moduleAccessSchema,
|
|
64
|
+
characters: moduleAccessSchema,
|
|
65
|
+
races: moduleAccessSchema,
|
|
66
|
+
organizations: moduleAccessSchema,
|
|
67
|
+
timeline: moduleAccessSchema,
|
|
68
|
+
relationships: moduleAccessSchema,
|
|
69
|
+
outlines: moduleAccessSchema,
|
|
70
|
+
reviews: moduleAccessSchema,
|
|
71
|
+
ai: moduleAccessSchema,
|
|
72
|
+
"ai-settings": moduleAccessSchema
|
|
73
|
+
}).strict();
|
|
74
|
+
const memberSchema = z.union([
|
|
75
|
+
z.object({ userId: identifier, permissions: modulePermissionsSchema }).strict(),
|
|
76
|
+
z.object({ userId: identifier, role: memberRoleValueSchema }).strict()
|
|
77
|
+
]);
|
|
78
|
+
const memberPermissionSchema = z.union([
|
|
79
|
+
z.object({ permissions: modulePermissionsSchema }).strict(),
|
|
80
|
+
z.object({ role: memberRoleValueSchema }).strict()
|
|
81
|
+
]);
|
|
60
82
|
const profileSchema = z.object({ displayName: z.string().trim().min(1).max(80) }).strict();
|
|
61
83
|
const passwordChangeSchema = z.object({ currentPassword: z.string().max(200), newPassword: passwordSchema }).strict();
|
|
62
84
|
const changeNoteSchema = z.string().trim().max(500).optional();
|
|
@@ -303,6 +325,55 @@ function noContent(response) {
|
|
|
303
325
|
function parse(schema, value) {
|
|
304
326
|
return schema.parse(value);
|
|
305
327
|
}
|
|
328
|
+
function recordValue(value) {
|
|
329
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
330
|
+
}
|
|
331
|
+
function mapRecords(value, mapper) {
|
|
332
|
+
if (Array.isArray(value))
|
|
333
|
+
return value.map((item) => recordValue(item) ? mapper(item) : item);
|
|
334
|
+
const record = recordValue(value);
|
|
335
|
+
if (!record)
|
|
336
|
+
return value;
|
|
337
|
+
if (Array.isArray(record.items)) {
|
|
338
|
+
return { ...record, items: record.items.map((item) => recordValue(item) ? mapper(item) : item) };
|
|
339
|
+
}
|
|
340
|
+
return mapper(record);
|
|
341
|
+
}
|
|
342
|
+
function redactCharacterLinks(record, permissions) {
|
|
343
|
+
const result = { ...record };
|
|
344
|
+
if (permissions.races === "none") {
|
|
345
|
+
result.raceId = null;
|
|
346
|
+
result.race = null;
|
|
347
|
+
result.species = "";
|
|
348
|
+
}
|
|
349
|
+
if (permissions.organizations === "none") {
|
|
350
|
+
result.organizationIds = [];
|
|
351
|
+
result.organizations = [];
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
function redactRaceMembers(record, permissions) {
|
|
356
|
+
return permissions.characters === "none" ? { ...record, memberIds: [], members: [] } : record;
|
|
357
|
+
}
|
|
358
|
+
function redactOrganizationMembers(record, permissions) {
|
|
359
|
+
return permissions.characters === "none" ? { ...record, memberIds: [], members: [] } : record;
|
|
360
|
+
}
|
|
361
|
+
function redactMergeRecords(value, mapper) {
|
|
362
|
+
const record = recordValue(value);
|
|
363
|
+
if (!record)
|
|
364
|
+
return value;
|
|
365
|
+
return {
|
|
366
|
+
...record,
|
|
367
|
+
...(recordValue(record.target) ? { target: mapper(record.target) } : {}),
|
|
368
|
+
...(recordValue(record.source) ? { source: mapper(record.source) } : {})
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function redactVersionSnapshots(value, mapper) {
|
|
372
|
+
return mapRecords(value, (version) => {
|
|
373
|
+
const snapshot = recordValue(version.snapshot);
|
|
374
|
+
return snapshot ? { ...version, snapshot: mapper(snapshot) } : version;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
306
377
|
export function createRuntime(options) {
|
|
307
378
|
logger.info("runtime.initializing", {
|
|
308
379
|
databasePath: options.databasePath,
|
|
@@ -319,6 +390,14 @@ export function createRuntime(options) {
|
|
|
319
390
|
mkdirSync(attachmentStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
|
|
320
391
|
const auth = new UserAuthService(database);
|
|
321
392
|
const store = new Store(database);
|
|
393
|
+
const requestPermissions = (request, workId) => {
|
|
394
|
+
if (!request.authUser)
|
|
395
|
+
return fullWorkModulePermissions();
|
|
396
|
+
const resolvedWorkId = workId ?? auth.resolveWorkId(request.path) ?? undefined;
|
|
397
|
+
if (!resolvedWorkId)
|
|
398
|
+
return fullWorkModulePermissions();
|
|
399
|
+
return auth.workModulePermissions(request.authUser, resolvedWorkId, request.authMethod !== "api-key") ?? fullWorkModulePermissions();
|
|
400
|
+
};
|
|
322
401
|
const captcha = new ImageCaptchaService({ revealAnswer: options.revealCaptchaAnswer === true });
|
|
323
402
|
const ai = new AiManager(store, new CredentialVault(options.masterSecret), options.fetchImpl ?? fetch, options.security ? (url) => assertSafeAiEndpoint(url, options.security?.allowPrivateAiEndpoints) : undefined);
|
|
324
403
|
const app = express();
|
|
@@ -536,19 +615,34 @@ export function createRuntime(options) {
|
|
|
536
615
|
if (!request.authUser)
|
|
537
616
|
throw new AppError(401, "AUTH_REQUIRED", "请先登录");
|
|
538
617
|
const input = parse(memberSchema, request.body);
|
|
539
|
-
const
|
|
540
|
-
|
|
618
|
+
const permissionInput = "permissions" in input
|
|
619
|
+
? { permissions: input.permissions }
|
|
620
|
+
: { role: input.role };
|
|
621
|
+
const members = database.transaction(() => {
|
|
622
|
+
const result = auth.addMember(request.params.workId, input.userId, permissionInput, request.authUser.userId);
|
|
623
|
+
store.audit(request.params.workId, "work.member-added", "user", input.userId, permissionInput);
|
|
624
|
+
return result;
|
|
625
|
+
});
|
|
541
626
|
data(response, members, 201);
|
|
542
627
|
});
|
|
543
628
|
app.patch("/api/works/:workId/members/:userId", (request, response) => {
|
|
544
|
-
const input = parse(
|
|
545
|
-
const
|
|
546
|
-
|
|
629
|
+
const input = parse(memberPermissionSchema, request.body);
|
|
630
|
+
const permissionInput = "permissions" in input
|
|
631
|
+
? { permissions: input.permissions }
|
|
632
|
+
: { role: input.role };
|
|
633
|
+
const members = database.transaction(() => {
|
|
634
|
+
const result = auth.updateMemberPermissions(request.params.workId, request.params.userId, permissionInput);
|
|
635
|
+
store.audit(request.params.workId, "work.member-role-updated", "user", request.params.userId, permissionInput);
|
|
636
|
+
return result;
|
|
637
|
+
});
|
|
547
638
|
data(response, members);
|
|
548
639
|
});
|
|
549
640
|
app.delete("/api/works/:workId/members/:userId", (request, response) => {
|
|
550
|
-
const members =
|
|
551
|
-
|
|
641
|
+
const members = database.transaction(() => {
|
|
642
|
+
const result = auth.removeMember(request.params.workId, request.params.userId);
|
|
643
|
+
store.audit(request.params.workId, "work.member-removed", "user", request.params.userId);
|
|
644
|
+
return result;
|
|
645
|
+
});
|
|
552
646
|
data(response, members);
|
|
553
647
|
});
|
|
554
648
|
app.patch("/api/works/:workId", (request, response) => {
|
|
@@ -731,23 +825,34 @@ export function createRuntime(options) {
|
|
|
731
825
|
includeMerged: z.enum(["0", "1"]).default("0")
|
|
732
826
|
}), request.query);
|
|
733
827
|
const pagination = parsePagination(request.query);
|
|
734
|
-
|
|
828
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
829
|
+
const characters = pagination
|
|
735
830
|
? store.listCharactersPage(request.params.workId, pagination, includeSections === "true", includeMerged === "1")
|
|
736
|
-
: store.listCharacters(request.params.workId, includeSections === "true", includeMerged === "1")
|
|
831
|
+
: store.listCharacters(request.params.workId, includeSections === "true", includeMerged === "1");
|
|
832
|
+
data(response, mapRecords(characters, (character) => redactCharacterLinks(character, permissions)));
|
|
833
|
+
});
|
|
834
|
+
app.post("/api/works/:workId/characters", (request, response) => {
|
|
835
|
+
const character = store.createCharacter(request.params.workId, parse(characterSchema, request.body));
|
|
836
|
+
data(response, redactCharacterLinks(character, requestPermissions(request, request.params.workId)), 201);
|
|
837
|
+
});
|
|
838
|
+
app.get("/api/characters/:characterId", (request, response) => {
|
|
839
|
+
data(response, redactCharacterLinks(store.getCharacter(request.params.characterId), requestPermissions(request)));
|
|
737
840
|
});
|
|
738
|
-
app.post("/api/works/:workId/characters", (request, response) => data(response, store.createCharacter(request.params.workId, parse(characterSchema, request.body)), 201));
|
|
739
|
-
app.get("/api/characters/:characterId", (request, response) => data(response, store.getCharacter(request.params.characterId)));
|
|
740
841
|
app.patch("/api/characters/:characterId", (request, response) => {
|
|
741
842
|
const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
|
|
742
|
-
|
|
843
|
+
const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
|
|
844
|
+
data(response, redactCharacterLinks(character, requestPermissions(request)));
|
|
743
845
|
});
|
|
744
846
|
app.get("/api/characters/:characterId/versions", (request, response) => {
|
|
745
847
|
const pagination = parsePagination(request.query);
|
|
746
|
-
|
|
848
|
+
const versions = pagination ? store.listCharacterVersionsPage(request.params.characterId, pagination) : store.listCharacterVersions(request.params.characterId);
|
|
849
|
+
const permissions = requestPermissions(request);
|
|
850
|
+
data(response, redactVersionSnapshots(versions, (snapshot) => redactCharacterLinks(snapshot, permissions)));
|
|
747
851
|
});
|
|
748
852
|
app.post("/api/characters/:characterId/restore", (request, response) => {
|
|
749
853
|
const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
750
|
-
|
|
854
|
+
const character = store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo);
|
|
855
|
+
data(response, redactCharacterLinks(character, requestPermissions(request)));
|
|
751
856
|
});
|
|
752
857
|
app.delete("/api/characters/:characterId", (request, response) => {
|
|
753
858
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
@@ -760,11 +865,13 @@ export function createRuntime(options) {
|
|
|
760
865
|
expectedTargetVersionNo: z.number().int().positive(),
|
|
761
866
|
expectedSourceVersionNo: z.number().int().positive()
|
|
762
867
|
}).strict(), request.body);
|
|
763
|
-
|
|
868
|
+
const result = store.mergeCharacters({
|
|
764
869
|
reviewId: null,
|
|
765
870
|
sourceCharacterId: request.params.characterId,
|
|
766
871
|
...input
|
|
767
|
-
})
|
|
872
|
+
});
|
|
873
|
+
const permissions = requestPermissions(request);
|
|
874
|
+
data(response, redactMergeRecords(result, (character) => redactCharacterLinks(character, permissions)));
|
|
768
875
|
});
|
|
769
876
|
app.get("/api/characters/:characterId/sections", (request, response) => {
|
|
770
877
|
const pagination = parsePagination(request.query);
|
|
@@ -844,15 +951,21 @@ export function createRuntime(options) {
|
|
|
844
951
|
});
|
|
845
952
|
app.get("/api/works/:workId/races", (request, response) => {
|
|
846
953
|
const pagination = parsePagination(request.query);
|
|
847
|
-
|
|
954
|
+
const races = pagination ? store.listRacesPage(request.params.workId, pagination) : store.listRaces(request.params.workId);
|
|
955
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
956
|
+
data(response, mapRecords(races, (race) => redactRaceMembers(race, permissions)));
|
|
848
957
|
});
|
|
849
958
|
app.post("/api/works/:workId/races", (request, response) => {
|
|
850
|
-
|
|
959
|
+
const race = store.createRace(request.params.workId, parse(raceSchema, request.body));
|
|
960
|
+
data(response, redactRaceMembers(race, requestPermissions(request, request.params.workId)), 201);
|
|
961
|
+
});
|
|
962
|
+
app.get("/api/races/:raceId", (request, response) => {
|
|
963
|
+
data(response, redactRaceMembers(store.getRace(request.params.raceId), requestPermissions(request)));
|
|
851
964
|
});
|
|
852
|
-
app.get("/api/races/:raceId", (request, response) => data(response, store.getRace(request.params.raceId)));
|
|
853
965
|
app.patch("/api/races/:raceId", (request, response) => {
|
|
854
966
|
const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
855
|
-
|
|
967
|
+
const race = store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo);
|
|
968
|
+
data(response, redactRaceMembers(race, requestPermissions(request)));
|
|
856
969
|
});
|
|
857
970
|
app.delete("/api/races/:raceId", (request, response) => {
|
|
858
971
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
@@ -861,19 +974,27 @@ export function createRuntime(options) {
|
|
|
861
974
|
});
|
|
862
975
|
app.post("/api/races/:raceId/merge", (request, response) => {
|
|
863
976
|
const input = parse(z.object({ targetRaceId: identifier }).strict(), request.body);
|
|
864
|
-
|
|
977
|
+
const result = store.mergeRaces(request.params.raceId, input.targetRaceId);
|
|
978
|
+
const permissions = requestPermissions(request);
|
|
979
|
+
data(response, redactMergeRecords(result, (race) => redactRaceMembers(race, permissions)));
|
|
865
980
|
});
|
|
866
981
|
app.get("/api/works/:workId/organizations", (request, response) => {
|
|
867
982
|
const pagination = parsePagination(request.query);
|
|
868
|
-
|
|
983
|
+
const organizations = pagination ? store.listOrganizationsPage(request.params.workId, pagination) : store.listOrganizations(request.params.workId);
|
|
984
|
+
const permissions = requestPermissions(request, request.params.workId);
|
|
985
|
+
data(response, mapRecords(organizations, (organization) => redactOrganizationMembers(organization, permissions)));
|
|
869
986
|
});
|
|
870
987
|
app.post("/api/works/:workId/organizations", (request, response) => {
|
|
871
|
-
|
|
988
|
+
const organization = store.createOrganization(request.params.workId, parse(organizationSchema, request.body));
|
|
989
|
+
data(response, redactOrganizationMembers(organization, requestPermissions(request, request.params.workId)), 201);
|
|
990
|
+
});
|
|
991
|
+
app.get("/api/organizations/:organizationId", (request, response) => {
|
|
992
|
+
data(response, redactOrganizationMembers(store.getOrganization(request.params.organizationId), requestPermissions(request)));
|
|
872
993
|
});
|
|
873
|
-
app.get("/api/organizations/:organizationId", (request, response) => data(response, store.getOrganization(request.params.organizationId)));
|
|
874
994
|
app.patch("/api/organizations/:organizationId", (request, response) => {
|
|
875
995
|
const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
876
|
-
|
|
996
|
+
const organization = store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo);
|
|
997
|
+
data(response, redactOrganizationMembers(organization, requestPermissions(request)));
|
|
877
998
|
});
|
|
878
999
|
app.delete("/api/organizations/:organizationId", (request, response) => {
|
|
879
1000
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
@@ -882,7 +1003,9 @@ export function createRuntime(options) {
|
|
|
882
1003
|
});
|
|
883
1004
|
app.post("/api/organizations/:organizationId/merge", (request, response) => {
|
|
884
1005
|
const input = parse(z.object({ targetOrganizationId: identifier }).strict(), request.body);
|
|
885
|
-
|
|
1006
|
+
const result = store.mergeOrganizations(request.params.organizationId, input.targetOrganizationId);
|
|
1007
|
+
const permissions = requestPermissions(request);
|
|
1008
|
+
data(response, redactMergeRecords(result, (organization) => redactOrganizationMembers(organization, permissions)));
|
|
886
1009
|
});
|
|
887
1010
|
app.get("/api/works/:workId/timeline-tracks", (request, response) => {
|
|
888
1011
|
const pagination = parsePagination(request.query);
|
|
@@ -961,9 +1084,19 @@ export function createRuntime(options) {
|
|
|
961
1084
|
app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
|
|
962
1085
|
const input = parse(z.object({ entityType: versionedEntityTypeSchema, entityId: identifier }), request.params);
|
|
963
1086
|
const pagination = parsePagination(request.query);
|
|
964
|
-
|
|
1087
|
+
const versions = pagination
|
|
965
1088
|
? store.listEntityVersionsPage(input.entityType, input.entityId, pagination)
|
|
966
|
-
: store.listEntityVersions(input.entityType, input.entityId)
|
|
1089
|
+
: store.listEntityVersions(input.entityType, input.entityId);
|
|
1090
|
+
const permissions = requestPermissions(request);
|
|
1091
|
+
if (input.entityType === "race") {
|
|
1092
|
+
data(response, redactVersionSnapshots(versions, (snapshot) => redactRaceMembers(snapshot, permissions)));
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (input.entityType === "organization") {
|
|
1096
|
+
data(response, redactVersionSnapshots(versions, (snapshot) => redactOrganizationMembers(snapshot, permissions)));
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
data(response, versions);
|
|
967
1100
|
});
|
|
968
1101
|
app.post("/api/entity-versions/:entityType/:entityId/restore", (request, response) => {
|
|
969
1102
|
const params = parse(z.object({ entityType: versionedEntityTypeSchema, entityId: identifier }), request.params);
|