@tinacms/graphql 2.4.11 → 4.0.0
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/ast-builder/index.d.ts +1 -0
- package/dist/database/index.d.ts +18 -0
- package/dist/index.js +429 -200
- package/dist/resolve.d.ts +5 -4
- package/dist/resolver/auth-fields.d.ts +48 -7
- package/dist/resolver/index.d.ts +32 -7
- package/package.json +4 -4
package/dist/database/index.d.ts
CHANGED
|
@@ -20,6 +20,16 @@ export interface DatabaseArgs {
|
|
|
20
20
|
version?: boolean;
|
|
21
21
|
namespace?: string;
|
|
22
22
|
levelBatchSize?: number;
|
|
23
|
+
authCollection?: AuthCollectionOptions;
|
|
24
|
+
}
|
|
25
|
+
export interface AuthCollectionOptions {
|
|
26
|
+
/** Values of the uid field of users who can edit the auth collection. */
|
|
27
|
+
admins?: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Allow writes to the auth collection from calls that pass no ctxUser.
|
|
30
|
+
* A null ctxUser means nobody is signed in, and is still refused.
|
|
31
|
+
*/
|
|
32
|
+
allowUnauthenticatedWrites?: boolean;
|
|
23
33
|
}
|
|
24
34
|
export interface GitProvider {
|
|
25
35
|
onPut: (key: string, value: string) => Promise<void>;
|
|
@@ -47,6 +57,10 @@ export type CreateLocalDatabaseArgs = Omit<DatabaseArgs, 'level'> & {
|
|
|
47
57
|
};
|
|
48
58
|
export declare const createLocalDatabase: (config?: CreateLocalDatabaseArgs) => Database;
|
|
49
59
|
export declare const createDatabase: (config: CreateDatabase) => Database;
|
|
60
|
+
/**
|
|
61
|
+
* Builds a database for CLI tooling and tests. It trusts requests without a
|
|
62
|
+
* user, so do not use it for a public endpoint. Use `createDatabase` instead.
|
|
63
|
+
*/
|
|
50
64
|
export declare const createDatabaseInternal: (config: DatabaseArgs) => Database;
|
|
51
65
|
/** Options for {@link Database.query} **/
|
|
52
66
|
export type QueryOptions = {
|
|
@@ -71,6 +85,10 @@ export declare class Database {
|
|
|
71
85
|
private readonly onPut;
|
|
72
86
|
private readonly onDelete;
|
|
73
87
|
private readonly levelBatchSize;
|
|
88
|
+
readonly authCollection: Readonly<{
|
|
89
|
+
admins: readonly string[];
|
|
90
|
+
allowUnauthenticatedWrites: boolean;
|
|
91
|
+
}>;
|
|
74
92
|
private tinaSchema;
|
|
75
93
|
private contentNamespace;
|
|
76
94
|
private collectionIndexDefinitions;
|
package/dist/index.js
CHANGED
|
@@ -505,6 +505,7 @@ var astBuilder = {
|
|
|
505
505
|
SystemInfo: "SystemInfo",
|
|
506
506
|
Boolean: "Boolean",
|
|
507
507
|
JSON: "JSON",
|
|
508
|
+
RichText: "RichText",
|
|
508
509
|
Node: "Node",
|
|
509
510
|
PageInfo: "PageInfo",
|
|
510
511
|
Connection: "Connection",
|
|
@@ -1173,6 +1174,10 @@ var scalarDefinitions = [
|
|
|
1173
1174
|
description: "References another document, used as a foreign key"
|
|
1174
1175
|
}),
|
|
1175
1176
|
astBuilder.ScalarTypeDefinition({ name: "JSON" }),
|
|
1177
|
+
astBuilder.ScalarTypeDefinition({
|
|
1178
|
+
name: "RichText",
|
|
1179
|
+
description: "A rich-text document, renderable by TinaMarkdown"
|
|
1180
|
+
}),
|
|
1176
1181
|
astBuilder.ObjectTypeDefinition({
|
|
1177
1182
|
name: "SystemInfo",
|
|
1178
1183
|
fields: [
|
|
@@ -2550,7 +2555,7 @@ var Builder = class {
|
|
|
2550
2555
|
return astBuilder.InputValueDefinition({
|
|
2551
2556
|
name: field.name,
|
|
2552
2557
|
list: field.list,
|
|
2553
|
-
type: astBuilder.TYPES.
|
|
2558
|
+
type: astBuilder.TYPES.RichText
|
|
2554
2559
|
});
|
|
2555
2560
|
case "reference":
|
|
2556
2561
|
return astBuilder.InputValueDefinition({
|
|
@@ -2799,7 +2804,7 @@ Visit https://tina.io/docs/r/content-fields/#list-fields/ for more information
|
|
|
2799
2804
|
name: field.name,
|
|
2800
2805
|
list: field.list,
|
|
2801
2806
|
required: field.required,
|
|
2802
|
-
type: astBuilder.TYPES.
|
|
2807
|
+
type: astBuilder.TYPES.RichText
|
|
2803
2808
|
});
|
|
2804
2809
|
case "reference":
|
|
2805
2810
|
const name = NAMER.documentTypeName(field.namespace);
|
|
@@ -3053,7 +3058,7 @@ var assertCheckboxGroupIsList = (field) => {
|
|
|
3053
3058
|
var package_default = {
|
|
3054
3059
|
name: "@tinacms/graphql",
|
|
3055
3060
|
type: "module",
|
|
3056
|
-
version: "
|
|
3061
|
+
version: "4.0.0",
|
|
3057
3062
|
main: "dist/index.js",
|
|
3058
3063
|
module: "./dist/index.js",
|
|
3059
3064
|
files: [
|
|
@@ -3343,7 +3348,7 @@ import {
|
|
|
3343
3348
|
} from "graphql";
|
|
3344
3349
|
|
|
3345
3350
|
// src/resolver/index.ts
|
|
3346
|
-
import
|
|
3351
|
+
import path4 from "path";
|
|
3347
3352
|
import { isValid } from "date-fns";
|
|
3348
3353
|
import { JSONPath as JSONPath2 } from "jsonpath-plus";
|
|
3349
3354
|
|
|
@@ -3351,7 +3356,11 @@ import { JSONPath as JSONPath2 } from "jsonpath-plus";
|
|
|
3351
3356
|
import { parseMDX, serializeMDX } from "@tinacms/mdx";
|
|
3352
3357
|
|
|
3353
3358
|
// src/resolver/index.ts
|
|
3354
|
-
import {
|
|
3359
|
+
import {
|
|
3360
|
+
ERR_ALREADY_EXISTS,
|
|
3361
|
+
RELATIVE_PATH_REGEX,
|
|
3362
|
+
normalizePath as normalizePath2
|
|
3363
|
+
} from "@tinacms/schema-tools";
|
|
3355
3364
|
import { GraphQLError as GraphQLError2 } from "graphql";
|
|
3356
3365
|
|
|
3357
3366
|
// src/database/datalayer.ts
|
|
@@ -4674,6 +4683,170 @@ var collectConditionsForField = (fieldName, field, filterNode, pathExpression, c
|
|
|
4674
4683
|
}
|
|
4675
4684
|
};
|
|
4676
4685
|
|
|
4686
|
+
// src/resolver/auth-fields.ts
|
|
4687
|
+
import { randomBytes } from "crypto";
|
|
4688
|
+
import path3 from "path";
|
|
4689
|
+
async function getUserDocumentContext(tinaSchema, resolver) {
|
|
4690
|
+
const collection = tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
4691
|
+
if (!collection) {
|
|
4692
|
+
throw new Error("Auth collection not found");
|
|
4693
|
+
}
|
|
4694
|
+
const userFields = mapUserFields(collection, ["_rawData"]);
|
|
4695
|
+
if (!userFields.length) {
|
|
4696
|
+
throw new Error(`No user field found in collection ${collection.name}`);
|
|
4697
|
+
}
|
|
4698
|
+
if (userFields.length > 1) {
|
|
4699
|
+
throw new Error(
|
|
4700
|
+
`Multiple user fields found in collection ${collection.name}`
|
|
4701
|
+
);
|
|
4702
|
+
}
|
|
4703
|
+
const userField = userFields[0];
|
|
4704
|
+
const relativePath = "index.json";
|
|
4705
|
+
const realPath = path3.join(collection.path, relativePath);
|
|
4706
|
+
const userDoc = await resolver.getDocument(realPath);
|
|
4707
|
+
const users = get(userDoc, userField.path);
|
|
4708
|
+
if (!users) {
|
|
4709
|
+
throw new Error("No users found");
|
|
4710
|
+
}
|
|
4711
|
+
return { collection, userField, users, userDoc, relativePath };
|
|
4712
|
+
}
|
|
4713
|
+
function findUserInCollection(users, userField, userSub) {
|
|
4714
|
+
const { idFieldName } = userField;
|
|
4715
|
+
if (!idFieldName) {
|
|
4716
|
+
throw new Error("No uid field found on user field");
|
|
4717
|
+
}
|
|
4718
|
+
return users.find((u) => u[idFieldName] === userSub) || null;
|
|
4719
|
+
}
|
|
4720
|
+
function withoutPasswordValue(user, userField) {
|
|
4721
|
+
const { passwordFieldName } = userField;
|
|
4722
|
+
if (!passwordFieldName) {
|
|
4723
|
+
return user;
|
|
4724
|
+
}
|
|
4725
|
+
return {
|
|
4726
|
+
...user,
|
|
4727
|
+
[passwordFieldName]: {
|
|
4728
|
+
passwordChangeRequired: user[passwordFieldName]?.passwordChangeRequired ?? false
|
|
4729
|
+
}
|
|
4730
|
+
};
|
|
4731
|
+
}
|
|
4732
|
+
var dummyPasswordHash;
|
|
4733
|
+
var warnedUnflaggedAuthenticate = false;
|
|
4734
|
+
var getDummyPasswordHash = () => {
|
|
4735
|
+
dummyPasswordHash ??= generatePasswordHash({
|
|
4736
|
+
password: randomBytes(32).toString("hex")
|
|
4737
|
+
});
|
|
4738
|
+
return dummyPasswordHash;
|
|
4739
|
+
};
|
|
4740
|
+
function assertNewUsersHavePasswords(collection, newBody, existingData) {
|
|
4741
|
+
const userFields = mapUserFields(collection);
|
|
4742
|
+
if (userFields.length !== 1) {
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
const [{ path: usersPath, idFieldName, passwordFieldName }] = userFields;
|
|
4746
|
+
const users = get(newBody, usersPath);
|
|
4747
|
+
if (!Array.isArray(users) || !idFieldName || !passwordFieldName) {
|
|
4748
|
+
return;
|
|
4749
|
+
}
|
|
4750
|
+
const storedUsers = get(existingData, usersPath);
|
|
4751
|
+
const storedIds = new Set(
|
|
4752
|
+
(Array.isArray(storedUsers) ? storedUsers : []).map((u) => u?.[idFieldName])
|
|
4753
|
+
);
|
|
4754
|
+
const withoutPassword = users.filter(
|
|
4755
|
+
(u) => !storedIds.has(u?.[idFieldName]) && !u?.[passwordFieldName]?.["value"]
|
|
4756
|
+
).map((u) => u?.[idFieldName]);
|
|
4757
|
+
if (withoutPassword.length) {
|
|
4758
|
+
throw new Error(`New users need a password: ${withoutPassword.join(", ")}`);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
async function handleAuthenticate({
|
|
4762
|
+
tinaSchema,
|
|
4763
|
+
resolver,
|
|
4764
|
+
sub,
|
|
4765
|
+
password,
|
|
4766
|
+
isSignIn
|
|
4767
|
+
}) {
|
|
4768
|
+
if (!isSignIn) {
|
|
4769
|
+
if (!warnedUnflaggedAuthenticate) {
|
|
4770
|
+
warnedUnflaggedAuthenticate = true;
|
|
4771
|
+
console.warn(
|
|
4772
|
+
"TinaCMS: authenticate was called without isSignIn, so it returned null. If sign-in is failing, run `tinacms build` with a @tinacms/cli that matches @tinacms/datalayer to regenerate the database client."
|
|
4773
|
+
);
|
|
4774
|
+
}
|
|
4775
|
+
return null;
|
|
4776
|
+
}
|
|
4777
|
+
const { userField, users } = await getUserDocumentContext(
|
|
4778
|
+
tinaSchema,
|
|
4779
|
+
resolver
|
|
4780
|
+
);
|
|
4781
|
+
const user = sub ? findUserInCollection(users, userField, sub) : null;
|
|
4782
|
+
const { passwordFieldName } = userField;
|
|
4783
|
+
const saltedHash = user ? get(user, [passwordFieldName || "", "value"]) : void 0;
|
|
4784
|
+
if (!saltedHash) {
|
|
4785
|
+
await checkPasswordHash({
|
|
4786
|
+
saltedHash: await getDummyPasswordHash(),
|
|
4787
|
+
password: password ?? ""
|
|
4788
|
+
});
|
|
4789
|
+
return null;
|
|
4790
|
+
}
|
|
4791
|
+
const matches = await checkPasswordHash({
|
|
4792
|
+
saltedHash,
|
|
4793
|
+
password
|
|
4794
|
+
});
|
|
4795
|
+
return matches ? withoutPasswordValue(user, userField) : null;
|
|
4796
|
+
}
|
|
4797
|
+
async function handleAuthorize({
|
|
4798
|
+
tinaSchema,
|
|
4799
|
+
resolver,
|
|
4800
|
+
sub,
|
|
4801
|
+
ctxUser
|
|
4802
|
+
}) {
|
|
4803
|
+
const userSub = sub || ctxUser?.sub;
|
|
4804
|
+
const { userField, users } = await getUserDocumentContext(
|
|
4805
|
+
tinaSchema,
|
|
4806
|
+
resolver
|
|
4807
|
+
);
|
|
4808
|
+
const user = findUserInCollection(users, userField, userSub);
|
|
4809
|
+
return user ? withoutPasswordValue(user, userField) : null;
|
|
4810
|
+
}
|
|
4811
|
+
async function handleUpdatePassword({
|
|
4812
|
+
tinaSchema,
|
|
4813
|
+
resolver,
|
|
4814
|
+
password,
|
|
4815
|
+
ctxUser
|
|
4816
|
+
}) {
|
|
4817
|
+
if (!ctxUser?.sub) {
|
|
4818
|
+
throw new Error("Not authorized");
|
|
4819
|
+
}
|
|
4820
|
+
if (!password) {
|
|
4821
|
+
throw new Error("No password provided");
|
|
4822
|
+
}
|
|
4823
|
+
const { userField, users } = await getUserDocumentContext(
|
|
4824
|
+
tinaSchema,
|
|
4825
|
+
resolver
|
|
4826
|
+
);
|
|
4827
|
+
const { idFieldName, passwordFieldName } = userField;
|
|
4828
|
+
if (!passwordFieldName) {
|
|
4829
|
+
throw new Error("No password field found on user field");
|
|
4830
|
+
}
|
|
4831
|
+
if (!users.find((u) => u[idFieldName] === ctxUser.sub)) {
|
|
4832
|
+
throw new Error("Not authorized");
|
|
4833
|
+
}
|
|
4834
|
+
const passwordHash = await generatePasswordHash({ password });
|
|
4835
|
+
await resolver.updateAuthDocumentInternal((rawData) => {
|
|
4836
|
+
const storedUsers = get(rawData, userField.path.slice(1));
|
|
4837
|
+
const user = Array.isArray(storedUsers) ? storedUsers.find((u) => u[idFieldName] === ctxUser.sub) : void 0;
|
|
4838
|
+
if (!user) {
|
|
4839
|
+
throw new Error("Not authorized");
|
|
4840
|
+
}
|
|
4841
|
+
user[passwordFieldName] = {
|
|
4842
|
+
value: passwordHash,
|
|
4843
|
+
passwordChangeRequired: false
|
|
4844
|
+
};
|
|
4845
|
+
return rawData;
|
|
4846
|
+
});
|
|
4847
|
+
return true;
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4677
4850
|
// src/resolver/media-utils.ts
|
|
4678
4851
|
var resolveMediaCloudToRelative = (value, config = { useRelativeMedia: true }, schema) => {
|
|
4679
4852
|
if (config && value) {
|
|
@@ -4755,6 +4928,7 @@ var hasTinaMediaConfig = (schema) => {
|
|
|
4755
4928
|
};
|
|
4756
4929
|
|
|
4757
4930
|
// src/resolver/index.ts
|
|
4931
|
+
var authCollectionWrites = /* @__PURE__ */ new WeakMap();
|
|
4758
4932
|
var createResolver = (args) => {
|
|
4759
4933
|
return new Resolver(args);
|
|
4760
4934
|
};
|
|
@@ -4884,6 +5058,7 @@ var resolveFieldData = async ({ namespace, ...field }, rawData, accumulator, tin
|
|
|
4884
5058
|
};
|
|
4885
5059
|
var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config, isAudit, hasReferences) => {
|
|
4886
5060
|
const collection = tinaSchema.getCollection(rawData._collection);
|
|
5061
|
+
const normalizedPath = normalizePath2(fullPath);
|
|
4887
5062
|
try {
|
|
4888
5063
|
const template = tinaSchema.getTemplateForData({
|
|
4889
5064
|
data: rawData,
|
|
@@ -4893,8 +5068,8 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
|
|
|
4893
5068
|
base: basename,
|
|
4894
5069
|
ext: extension,
|
|
4895
5070
|
name: filename
|
|
4896
|
-
} =
|
|
4897
|
-
const relativePath =
|
|
5071
|
+
} = path4.parse(normalizedPath);
|
|
5072
|
+
const relativePath = normalizedPath.replace(collection.path, "").replace(/^\/|\/$/g, "");
|
|
4898
5073
|
const breadcrumbs = relativePath.replace(extension, "").split("/");
|
|
4899
5074
|
const data = {
|
|
4900
5075
|
_collection: rawData._collection,
|
|
@@ -4928,7 +5103,7 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
|
|
|
4928
5103
|
const title = data[titleFieldName || " "] || null;
|
|
4929
5104
|
return {
|
|
4930
5105
|
__typename: collection.fields ? NAMER.documentTypeName(collection.namespace) : NAMER.documentTypeName(template.namespace),
|
|
4931
|
-
id:
|
|
5106
|
+
id: normalizedPath,
|
|
4932
5107
|
...data,
|
|
4933
5108
|
_sys: {
|
|
4934
5109
|
title: title || "",
|
|
@@ -4936,7 +5111,7 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
|
|
|
4936
5111
|
filename,
|
|
4937
5112
|
extension,
|
|
4938
5113
|
hasReferences,
|
|
4939
|
-
path:
|
|
5114
|
+
path: normalizedPath,
|
|
4940
5115
|
relativePath,
|
|
4941
5116
|
breadcrumbs,
|
|
4942
5117
|
collection,
|
|
@@ -4990,12 +5165,14 @@ var Resolver = class _Resolver {
|
|
|
4990
5165
|
this.database = init.database;
|
|
4991
5166
|
this.tinaSchema = init.tinaSchema;
|
|
4992
5167
|
this.isAudit = init.isAudit;
|
|
5168
|
+
this.ctxUser = init.ctxUser;
|
|
4993
5169
|
}
|
|
4994
5170
|
init;
|
|
4995
5171
|
config;
|
|
4996
5172
|
database;
|
|
4997
5173
|
tinaSchema;
|
|
4998
5174
|
isAudit;
|
|
5175
|
+
ctxUser;
|
|
4999
5176
|
resolveCollection = async (_args, collectionName, hasDocuments) => {
|
|
5000
5177
|
const collection = this.tinaSchema.getCollection(collectionName);
|
|
5001
5178
|
const extraFields = {};
|
|
@@ -5172,6 +5349,11 @@ var Resolver = class _Resolver {
|
|
|
5172
5349
|
collectionName,
|
|
5173
5350
|
relativePath
|
|
5174
5351
|
);
|
|
5352
|
+
await this.assertAuthCollectionWrite({
|
|
5353
|
+
collection,
|
|
5354
|
+
realPath,
|
|
5355
|
+
op: "addPending"
|
|
5356
|
+
});
|
|
5175
5357
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5176
5358
|
if (alreadyExists) {
|
|
5177
5359
|
throw new Error(
|
|
@@ -5287,19 +5469,19 @@ var Resolver = class _Resolver {
|
|
|
5287
5469
|
if (fullPath.includes("\0")) {
|
|
5288
5470
|
throw new Error("Invalid path: null bytes are not allowed");
|
|
5289
5471
|
}
|
|
5290
|
-
const normalizedPath =
|
|
5291
|
-
const normalizedCollectionPath =
|
|
5292
|
-
const relative =
|
|
5472
|
+
const normalizedPath = path4.normalize(fullPath.replace(/\\/g, "/"));
|
|
5473
|
+
const normalizedCollectionPath = path4.normalize(collection.path);
|
|
5474
|
+
const relative = path4.relative(normalizedCollectionPath, normalizedPath);
|
|
5293
5475
|
if (relative.startsWith("..")) {
|
|
5294
5476
|
throw new Error(`Invalid path: path escapes the collection directory`);
|
|
5295
5477
|
}
|
|
5296
|
-
if (
|
|
5478
|
+
if (path4.isAbsolute(relative)) {
|
|
5297
5479
|
throw new Error(`Invalid path: absolute paths are not allowed`);
|
|
5298
5480
|
}
|
|
5299
5481
|
if (relativePath) {
|
|
5300
5482
|
_Resolver.validateRelativePath(relativePath);
|
|
5301
5483
|
const collectionFormat = collection.format || "md";
|
|
5302
|
-
const fileExtension =
|
|
5484
|
+
const fileExtension = path4.extname(relativePath).toLowerCase().slice(1);
|
|
5303
5485
|
if (fileExtension !== collectionFormat) {
|
|
5304
5486
|
throw new Error(
|
|
5305
5487
|
`Invalid file extension: expected '.${collectionFormat}' but got '.${fileExtension}'`
|
|
@@ -5307,6 +5489,81 @@ var Resolver = class _Resolver {
|
|
|
5307
5489
|
}
|
|
5308
5490
|
}
|
|
5309
5491
|
};
|
|
5492
|
+
assertAuthCollectionWrite = async ({
|
|
5493
|
+
collection,
|
|
5494
|
+
realPath,
|
|
5495
|
+
op
|
|
5496
|
+
}) => {
|
|
5497
|
+
const authCollection = this.tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
5498
|
+
if (!authCollection) {
|
|
5499
|
+
return;
|
|
5500
|
+
}
|
|
5501
|
+
const normalizedPath = normalizePath2(realPath);
|
|
5502
|
+
if (collection.name !== authCollection.name && !this.isStoredInCollection(normalizedPath, authCollection)) {
|
|
5503
|
+
return;
|
|
5504
|
+
}
|
|
5505
|
+
const { admins, allowUnauthenticatedWrites } = this.database.authCollection ?? { admins: [], allowUnauthenticatedWrites: true };
|
|
5506
|
+
if (this.ctxUser === void 0 && allowUnauthenticatedWrites) {
|
|
5507
|
+
return;
|
|
5508
|
+
}
|
|
5509
|
+
const sub = this.ctxUser?.sub;
|
|
5510
|
+
const userStorePath = normalizePath2(
|
|
5511
|
+
path4.join(authCollection.path, "index.json")
|
|
5512
|
+
);
|
|
5513
|
+
if (op === "update" && normalizedPath === userStorePath && typeof sub === "string" && sub !== "" && admins.includes(sub) && await this.isStoredUser(sub)) {
|
|
5514
|
+
return;
|
|
5515
|
+
}
|
|
5516
|
+
throw new Error("Not authorized");
|
|
5517
|
+
};
|
|
5518
|
+
assertReferenceUpdates = async (realPath, collection) => {
|
|
5519
|
+
if (!this.tinaSchema.getCollections().some((c) => c.isAuthCollection)) {
|
|
5520
|
+
return;
|
|
5521
|
+
}
|
|
5522
|
+
const collRefs = await this.findReferences(realPath, collection);
|
|
5523
|
+
for (const docsWithRefs of Object.values(collRefs)) {
|
|
5524
|
+
for (const pathToDocWithRef of Object.keys(docsWithRefs)) {
|
|
5525
|
+
await this.assertAuthCollectionWrite({
|
|
5526
|
+
collection: this.tinaSchema.getCollectionByFullPath(pathToDocWithRef),
|
|
5527
|
+
realPath: pathToDocWithRef,
|
|
5528
|
+
op: "update"
|
|
5529
|
+
});
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
};
|
|
5533
|
+
isAuthCollectionPath = (realPath) => {
|
|
5534
|
+
const authCollection = this.tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
5535
|
+
return !!authCollection && this.isStoredInCollection(normalizePath2(realPath), authCollection);
|
|
5536
|
+
};
|
|
5537
|
+
withAuthCollectionLock = (realPath, write) => {
|
|
5538
|
+
if (!this.isAuthCollectionPath(realPath)) {
|
|
5539
|
+
return write();
|
|
5540
|
+
}
|
|
5541
|
+
const previous = authCollectionWrites.get(this.database);
|
|
5542
|
+
const current = previous ? previous.then(write, write) : write();
|
|
5543
|
+
authCollectionWrites.set(
|
|
5544
|
+
this.database,
|
|
5545
|
+
current.catch(() => void 0)
|
|
5546
|
+
);
|
|
5547
|
+
return current;
|
|
5548
|
+
};
|
|
5549
|
+
isStoredInCollection = (realPath, collection) => {
|
|
5550
|
+
try {
|
|
5551
|
+
return this.tinaSchema.getCollectionByFullPath(realPath)?.name === collection.name;
|
|
5552
|
+
} catch {
|
|
5553
|
+
return false;
|
|
5554
|
+
}
|
|
5555
|
+
};
|
|
5556
|
+
isStoredUser = async (sub) => {
|
|
5557
|
+
try {
|
|
5558
|
+
const { userField, users } = await getUserDocumentContext(
|
|
5559
|
+
this.tinaSchema,
|
|
5560
|
+
this
|
|
5561
|
+
);
|
|
5562
|
+
return Array.isArray(users) && !!findUserInCollection(users, userField, sub);
|
|
5563
|
+
} catch {
|
|
5564
|
+
return false;
|
|
5565
|
+
}
|
|
5566
|
+
};
|
|
5310
5567
|
/**
|
|
5311
5568
|
* Helper method to get collection and construct validated path.
|
|
5312
5569
|
* This encapsulates the common pattern of getting a collection, joining paths,
|
|
@@ -5315,7 +5572,9 @@ var Resolver = class _Resolver {
|
|
|
5315
5572
|
* @param collectionName - Name of the collection
|
|
5316
5573
|
* @param relativePath - Relative path within the collection
|
|
5317
5574
|
* @param options - Optional configuration
|
|
5318
|
-
* @returns Object containing the collection and validated real path
|
|
5575
|
+
* @returns Object containing the collection and the validated real path,
|
|
5576
|
+
* normalized to POSIX separators so it matches the document identities and
|
|
5577
|
+
* reference values stored in content files on every platform
|
|
5319
5578
|
*/
|
|
5320
5579
|
getValidatedPath = (collectionName, relativePath, options) => {
|
|
5321
5580
|
_Resolver.validateRelativePath(relativePath);
|
|
@@ -5325,7 +5584,7 @@ var Resolver = class _Resolver {
|
|
|
5325
5584
|
if (options?.extraSegments) {
|
|
5326
5585
|
pathSegments.push(...options.extraSegments.map(_Resolver.sanitizePath));
|
|
5327
5586
|
}
|
|
5328
|
-
const realPath =
|
|
5587
|
+
const realPath = normalizePath2(path4.join(...pathSegments));
|
|
5329
5588
|
const shouldValidateExtension = options?.validateExtension !== false;
|
|
5330
5589
|
this.validatePath(
|
|
5331
5590
|
realPath,
|
|
@@ -5359,12 +5618,17 @@ var Resolver = class _Resolver {
|
|
|
5359
5618
|
}) => {
|
|
5360
5619
|
const collection = this.getCollectionWithName(collectionName);
|
|
5361
5620
|
_Resolver.validateRelativePath(relativePath);
|
|
5362
|
-
const realPath =
|
|
5621
|
+
const realPath = path4.join(
|
|
5363
5622
|
collection.path,
|
|
5364
5623
|
relativePath,
|
|
5365
5624
|
`.gitkeep.${collection.format || "md"}`
|
|
5366
5625
|
);
|
|
5367
5626
|
this.validatePath(realPath, collection);
|
|
5627
|
+
await this.assertAuthCollectionWrite({
|
|
5628
|
+
collection,
|
|
5629
|
+
realPath,
|
|
5630
|
+
op: "createFolder"
|
|
5631
|
+
});
|
|
5368
5632
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5369
5633
|
if (alreadyExists) {
|
|
5370
5634
|
throw new Error(
|
|
@@ -5387,6 +5651,11 @@ var Resolver = class _Resolver {
|
|
|
5387
5651
|
collectionName,
|
|
5388
5652
|
relativePath
|
|
5389
5653
|
);
|
|
5654
|
+
await this.assertAuthCollectionWrite({
|
|
5655
|
+
collection,
|
|
5656
|
+
realPath,
|
|
5657
|
+
op: "create"
|
|
5658
|
+
});
|
|
5390
5659
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5391
5660
|
if (alreadyExists) {
|
|
5392
5661
|
throw new Error(
|
|
@@ -5397,7 +5666,17 @@ var Resolver = class _Resolver {
|
|
|
5397
5666
|
await this.database.put(realPath, params, collection.name);
|
|
5398
5667
|
return this.getDocument(realPath);
|
|
5399
5668
|
};
|
|
5400
|
-
resolveUpdateDocument = async ({
|
|
5669
|
+
resolveUpdateDocument = async (args) => {
|
|
5670
|
+
const { realPath } = this.getValidatedPath(
|
|
5671
|
+
args.collectionName,
|
|
5672
|
+
args.relativePath
|
|
5673
|
+
);
|
|
5674
|
+
return this.withAuthCollectionLock(
|
|
5675
|
+
realPath,
|
|
5676
|
+
() => this.updateDocument(args)
|
|
5677
|
+
);
|
|
5678
|
+
};
|
|
5679
|
+
updateDocument = async ({
|
|
5401
5680
|
collectionName,
|
|
5402
5681
|
relativePath,
|
|
5403
5682
|
newRelativePath,
|
|
@@ -5407,6 +5686,13 @@ var Resolver = class _Resolver {
|
|
|
5407
5686
|
collectionName,
|
|
5408
5687
|
relativePath
|
|
5409
5688
|
);
|
|
5689
|
+
if (!newRelativePath) {
|
|
5690
|
+
await this.assertAuthCollectionWrite({
|
|
5691
|
+
collection,
|
|
5692
|
+
realPath,
|
|
5693
|
+
op: "update"
|
|
5694
|
+
});
|
|
5695
|
+
}
|
|
5410
5696
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5411
5697
|
if (!alreadyExists) {
|
|
5412
5698
|
throw new Error(`Unable to update document, ${realPath} does not exist`);
|
|
@@ -5426,38 +5712,53 @@ var Resolver = class _Resolver {
|
|
|
5426
5712
|
`Unable to rename document, ${newRealPath} ${ERR_ALREADY_EXISTS}`
|
|
5427
5713
|
);
|
|
5428
5714
|
}
|
|
5715
|
+
await this.assertAuthCollectionWrite({
|
|
5716
|
+
collection,
|
|
5717
|
+
realPath,
|
|
5718
|
+
op: "rename"
|
|
5719
|
+
});
|
|
5720
|
+
await this.assertAuthCollectionWrite({
|
|
5721
|
+
collection,
|
|
5722
|
+
realPath: newRealPath,
|
|
5723
|
+
op: "rename"
|
|
5724
|
+
});
|
|
5725
|
+
await this.assertReferenceUpdates(realPath, collection);
|
|
5429
5726
|
await this.database.put(newRealPath, doc._rawData, collection.name);
|
|
5430
5727
|
await this.deleteDocument(realPath);
|
|
5431
5728
|
const collRefs = await this.findReferences(realPath, collection);
|
|
5729
|
+
const holdsLock = this.isAuthCollectionPath(realPath);
|
|
5432
5730
|
for (const [_collection, docsWithRefs] of Object.entries(collRefs)) {
|
|
5433
5731
|
for (const [pathToDocWithRef, referencePaths] of Object.entries(
|
|
5434
5732
|
docsWithRefs
|
|
5435
5733
|
)) {
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
const
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
docWithRef = object2;
|
|
5446
|
-
hasUpdate = updated || hasUpdate;
|
|
5447
|
-
}
|
|
5448
|
-
if (hasUpdate) {
|
|
5449
|
-
const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
|
|
5450
|
-
if (!collectionWithRef) {
|
|
5451
|
-
throw new Error(
|
|
5452
|
-
`Unable to find collection for ${pathToDocWithRef}`
|
|
5734
|
+
const rewriteReferences = async () => {
|
|
5735
|
+
let docWithRef = await this.getRaw(pathToDocWithRef);
|
|
5736
|
+
let hasUpdate = false;
|
|
5737
|
+
for (const path9 of referencePaths) {
|
|
5738
|
+
const { object: object2, updated } = updateObjectWithJsonPath(
|
|
5739
|
+
docWithRef,
|
|
5740
|
+
path9,
|
|
5741
|
+
realPath,
|
|
5742
|
+
newRealPath
|
|
5453
5743
|
);
|
|
5744
|
+
docWithRef = object2;
|
|
5745
|
+
hasUpdate = updated || hasUpdate;
|
|
5454
5746
|
}
|
|
5455
|
-
|
|
5456
|
-
pathToDocWithRef
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5747
|
+
if (hasUpdate) {
|
|
5748
|
+
const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
|
|
5749
|
+
if (!collectionWithRef) {
|
|
5750
|
+
throw new Error(
|
|
5751
|
+
`Unable to find collection for ${pathToDocWithRef}`
|
|
5752
|
+
);
|
|
5753
|
+
}
|
|
5754
|
+
await this.database.put(
|
|
5755
|
+
pathToDocWithRef,
|
|
5756
|
+
docWithRef,
|
|
5757
|
+
collectionWithRef.name
|
|
5758
|
+
);
|
|
5759
|
+
}
|
|
5760
|
+
};
|
|
5761
|
+
await (holdsLock ? rewriteReferences() : this.withAuthCollectionLock(pathToDocWithRef, rewriteReferences));
|
|
5461
5762
|
}
|
|
5462
5763
|
}
|
|
5463
5764
|
return this.getDocument(newRealPath);
|
|
@@ -5465,6 +5766,9 @@ var Resolver = class _Resolver {
|
|
|
5465
5766
|
if (!newBody) {
|
|
5466
5767
|
throw new Error("Body not provided for updated document.");
|
|
5467
5768
|
}
|
|
5769
|
+
if (collection.isAuthCollection) {
|
|
5770
|
+
assertNewUsersHavePasswords(collection, newBody, doc?._rawData);
|
|
5771
|
+
}
|
|
5468
5772
|
const params = await this.buildObjectMutations(
|
|
5469
5773
|
newBody,
|
|
5470
5774
|
collection,
|
|
@@ -5481,6 +5785,21 @@ var Resolver = class _Resolver {
|
|
|
5481
5785
|
);
|
|
5482
5786
|
return this.getDocument(realPath);
|
|
5483
5787
|
};
|
|
5788
|
+
/**
|
|
5789
|
+
* Rewrites the auth collection's user store without the admin check, so the
|
|
5790
|
+
* caller must check the request user.
|
|
5791
|
+
*/
|
|
5792
|
+
updateAuthDocumentInternal = async (update) => {
|
|
5793
|
+
const collection = this.tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
5794
|
+
if (!collection) {
|
|
5795
|
+
throw new Error("Auth collection not found");
|
|
5796
|
+
}
|
|
5797
|
+
const { realPath } = this.getValidatedPath(collection.name, "index.json");
|
|
5798
|
+
await this.withAuthCollectionLock(realPath, async () => {
|
|
5799
|
+
const rawData = await this.getRaw(realPath);
|
|
5800
|
+
await this.database.put(realPath, update(rawData), collection.name);
|
|
5801
|
+
});
|
|
5802
|
+
};
|
|
5484
5803
|
resolveDeleteDocument = async ({
|
|
5485
5804
|
collectionName,
|
|
5486
5805
|
relativePath
|
|
@@ -5493,39 +5812,50 @@ var Resolver = class _Resolver {
|
|
|
5493
5812
|
if (!alreadyExists) {
|
|
5494
5813
|
throw new Error(`Unable to delete document, ${realPath} does not exist`);
|
|
5495
5814
|
}
|
|
5815
|
+
await this.assertAuthCollectionWrite({
|
|
5816
|
+
collection,
|
|
5817
|
+
realPath,
|
|
5818
|
+
op: "delete"
|
|
5819
|
+
});
|
|
5820
|
+
const hasReferences = await this.hasReferences(realPath, collection);
|
|
5821
|
+
if (hasReferences) {
|
|
5822
|
+
await this.assertReferenceUpdates(realPath, collection);
|
|
5823
|
+
}
|
|
5496
5824
|
const doc = await this.getDocument(realPath);
|
|
5497
5825
|
await this.deleteDocument(realPath);
|
|
5498
|
-
if (
|
|
5826
|
+
if (hasReferences) {
|
|
5499
5827
|
const collRefs = await this.findReferences(realPath, collection);
|
|
5500
5828
|
for (const [_collection, docsWithRefs] of Object.entries(collRefs)) {
|
|
5501
5829
|
for (const [pathToDocWithRef, referencePaths] of Object.entries(
|
|
5502
5830
|
docsWithRefs
|
|
5503
5831
|
)) {
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
const
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
refDoc = object2;
|
|
5514
|
-
hasUpdate = updated || hasUpdate;
|
|
5515
|
-
}
|
|
5516
|
-
if (hasUpdate) {
|
|
5517
|
-
const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
|
|
5518
|
-
if (!collectionWithRef) {
|
|
5519
|
-
throw new Error(
|
|
5520
|
-
`Unable to find collection for ${pathToDocWithRef}`
|
|
5832
|
+
await this.withAuthCollectionLock(pathToDocWithRef, async () => {
|
|
5833
|
+
let refDoc = await this.getRaw(pathToDocWithRef);
|
|
5834
|
+
let hasUpdate = false;
|
|
5835
|
+
for (const path9 of referencePaths) {
|
|
5836
|
+
const { object: object2, updated } = updateObjectWithJsonPath(
|
|
5837
|
+
refDoc,
|
|
5838
|
+
path9,
|
|
5839
|
+
realPath,
|
|
5840
|
+
null
|
|
5521
5841
|
);
|
|
5842
|
+
refDoc = object2;
|
|
5843
|
+
hasUpdate = updated || hasUpdate;
|
|
5522
5844
|
}
|
|
5523
|
-
|
|
5524
|
-
pathToDocWithRef
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5845
|
+
if (hasUpdate) {
|
|
5846
|
+
const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
|
|
5847
|
+
if (!collectionWithRef) {
|
|
5848
|
+
throw new Error(
|
|
5849
|
+
`Unable to find collection for ${pathToDocWithRef}`
|
|
5850
|
+
);
|
|
5851
|
+
}
|
|
5852
|
+
await this.database.put(
|
|
5853
|
+
pathToDocWithRef,
|
|
5854
|
+
refDoc,
|
|
5855
|
+
collectionWithRef.name
|
|
5856
|
+
);
|
|
5857
|
+
}
|
|
5858
|
+
});
|
|
5529
5859
|
}
|
|
5530
5860
|
}
|
|
5531
5861
|
}
|
|
@@ -5561,10 +5891,15 @@ var Resolver = class _Resolver {
|
|
|
5561
5891
|
if (isAddPendingDocument === true) {
|
|
5562
5892
|
return this.resolveAddPendingDocument({
|
|
5563
5893
|
collectionName: collection.name,
|
|
5564
|
-
relativePath:
|
|
5894
|
+
relativePath: path4.relative(collection.path, realPath),
|
|
5565
5895
|
templateName: args.template
|
|
5566
5896
|
});
|
|
5567
5897
|
}
|
|
5898
|
+
await this.assertAuthCollectionWrite({
|
|
5899
|
+
collection,
|
|
5900
|
+
realPath,
|
|
5901
|
+
op: "create"
|
|
5902
|
+
});
|
|
5568
5903
|
const params = await this.buildObjectMutations(
|
|
5569
5904
|
// @ts-ignore
|
|
5570
5905
|
args.params[collection.name],
|
|
@@ -5583,6 +5918,11 @@ var Resolver = class _Resolver {
|
|
|
5583
5918
|
isAddPendingDocument,
|
|
5584
5919
|
isCollectionSpecific
|
|
5585
5920
|
}) => {
|
|
5921
|
+
await this.assertAuthCollectionWrite({
|
|
5922
|
+
collection,
|
|
5923
|
+
realPath,
|
|
5924
|
+
op: "update"
|
|
5925
|
+
});
|
|
5586
5926
|
const doc = await this.getDocument(realPath);
|
|
5587
5927
|
const oldDoc = this.resolveLegacyValues(doc?._rawData || {}, collection);
|
|
5588
5928
|
if (isAddPendingDocument === true) {
|
|
@@ -5893,7 +6233,7 @@ var Resolver = class _Resolver {
|
|
|
5893
6233
|
filterExpression: {
|
|
5894
6234
|
_type: "string",
|
|
5895
6235
|
_list: false,
|
|
5896
|
-
eq: id
|
|
6236
|
+
eq: normalizePath2(id)
|
|
5897
6237
|
}
|
|
5898
6238
|
}
|
|
5899
6239
|
]
|
|
@@ -5928,7 +6268,7 @@ var Resolver = class _Resolver {
|
|
|
5928
6268
|
filterExpression: {
|
|
5929
6269
|
_type: "string",
|
|
5930
6270
|
_list: false,
|
|
5931
|
-
eq: id
|
|
6271
|
+
eq: normalizePath2(id)
|
|
5932
6272
|
}
|
|
5933
6273
|
}
|
|
5934
6274
|
]
|
|
@@ -6089,130 +6429,6 @@ var resolveDateInput = (value) => {
|
|
|
6089
6429
|
return date;
|
|
6090
6430
|
};
|
|
6091
6431
|
|
|
6092
|
-
// src/resolver/auth-fields.ts
|
|
6093
|
-
import path4 from "path";
|
|
6094
|
-
import { set } from "es-toolkit/compat";
|
|
6095
|
-
async function getUserDocumentContext(tinaSchema, resolver) {
|
|
6096
|
-
const collection = tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
6097
|
-
if (!collection) {
|
|
6098
|
-
throw new Error("Auth collection not found");
|
|
6099
|
-
}
|
|
6100
|
-
const userFields = mapUserFields(collection, ["_rawData"]);
|
|
6101
|
-
if (!userFields.length) {
|
|
6102
|
-
throw new Error(`No user field found in collection ${collection.name}`);
|
|
6103
|
-
}
|
|
6104
|
-
if (userFields.length > 1) {
|
|
6105
|
-
throw new Error(
|
|
6106
|
-
`Multiple user fields found in collection ${collection.name}`
|
|
6107
|
-
);
|
|
6108
|
-
}
|
|
6109
|
-
const userField = userFields[0];
|
|
6110
|
-
const relativePath = "index.json";
|
|
6111
|
-
const realPath = path4.join(collection.path, relativePath);
|
|
6112
|
-
const userDoc = await resolver.getDocument(realPath);
|
|
6113
|
-
const users = get(userDoc, userField.path);
|
|
6114
|
-
if (!users) {
|
|
6115
|
-
throw new Error("No users found");
|
|
6116
|
-
}
|
|
6117
|
-
return { collection, userField, users, userDoc, relativePath };
|
|
6118
|
-
}
|
|
6119
|
-
function findUserInCollection(users, userField, userSub) {
|
|
6120
|
-
const { idFieldName } = userField;
|
|
6121
|
-
if (!idFieldName) {
|
|
6122
|
-
throw new Error("No uid field found on user field");
|
|
6123
|
-
}
|
|
6124
|
-
return users.find((u) => u[idFieldName] === userSub) || null;
|
|
6125
|
-
}
|
|
6126
|
-
async function handleAuthenticate({
|
|
6127
|
-
tinaSchema,
|
|
6128
|
-
resolver,
|
|
6129
|
-
sub,
|
|
6130
|
-
password,
|
|
6131
|
-
ctxUser
|
|
6132
|
-
}) {
|
|
6133
|
-
const userSub = sub || ctxUser?.sub;
|
|
6134
|
-
const { userField, users } = await getUserDocumentContext(
|
|
6135
|
-
tinaSchema,
|
|
6136
|
-
resolver
|
|
6137
|
-
);
|
|
6138
|
-
const user = findUserInCollection(users, userField, userSub);
|
|
6139
|
-
if (!user) {
|
|
6140
|
-
return null;
|
|
6141
|
-
}
|
|
6142
|
-
const { passwordFieldName } = userField;
|
|
6143
|
-
const saltedHash = get(user, [passwordFieldName || "", "value"]);
|
|
6144
|
-
if (!saltedHash) {
|
|
6145
|
-
throw new Error("No password field found on user field");
|
|
6146
|
-
}
|
|
6147
|
-
const matches = await checkPasswordHash({
|
|
6148
|
-
saltedHash,
|
|
6149
|
-
password
|
|
6150
|
-
});
|
|
6151
|
-
return matches ? user : null;
|
|
6152
|
-
}
|
|
6153
|
-
async function handleAuthorize({
|
|
6154
|
-
tinaSchema,
|
|
6155
|
-
resolver,
|
|
6156
|
-
sub,
|
|
6157
|
-
ctxUser
|
|
6158
|
-
}) {
|
|
6159
|
-
const userSub = sub || ctxUser?.sub;
|
|
6160
|
-
const { userField, users } = await getUserDocumentContext(
|
|
6161
|
-
tinaSchema,
|
|
6162
|
-
resolver
|
|
6163
|
-
);
|
|
6164
|
-
const user = findUserInCollection(users, userField, userSub);
|
|
6165
|
-
return user ? user : null;
|
|
6166
|
-
}
|
|
6167
|
-
async function handleUpdatePassword({
|
|
6168
|
-
tinaSchema,
|
|
6169
|
-
resolver,
|
|
6170
|
-
password,
|
|
6171
|
-
ctxUser
|
|
6172
|
-
}) {
|
|
6173
|
-
if (!ctxUser?.sub) {
|
|
6174
|
-
throw new Error("Not authorized");
|
|
6175
|
-
}
|
|
6176
|
-
if (!password) {
|
|
6177
|
-
throw new Error("No password provided");
|
|
6178
|
-
}
|
|
6179
|
-
const { collection, userField, users, relativePath } = await getUserDocumentContext(tinaSchema, resolver);
|
|
6180
|
-
const { idFieldName, passwordFieldName } = userField;
|
|
6181
|
-
const user = users.find((u) => u[idFieldName] === ctxUser.sub);
|
|
6182
|
-
if (!user) {
|
|
6183
|
-
throw new Error("Not authorized");
|
|
6184
|
-
}
|
|
6185
|
-
user[passwordFieldName] = {
|
|
6186
|
-
value: password,
|
|
6187
|
-
passwordChangeRequired: false
|
|
6188
|
-
};
|
|
6189
|
-
const newBody = {};
|
|
6190
|
-
set(
|
|
6191
|
-
newBody,
|
|
6192
|
-
userField.path.slice(1),
|
|
6193
|
-
// remove _rawData from users path
|
|
6194
|
-
users.map((u) => {
|
|
6195
|
-
if (user[idFieldName] === u[idFieldName]) {
|
|
6196
|
-
return user;
|
|
6197
|
-
}
|
|
6198
|
-
return {
|
|
6199
|
-
// don't overwrite other users' passwords
|
|
6200
|
-
...u,
|
|
6201
|
-
[passwordFieldName]: {
|
|
6202
|
-
...u[passwordFieldName],
|
|
6203
|
-
value: ""
|
|
6204
|
-
}
|
|
6205
|
-
};
|
|
6206
|
-
})
|
|
6207
|
-
);
|
|
6208
|
-
await resolver.resolveUpdateDocument({
|
|
6209
|
-
collectionName: collection.name,
|
|
6210
|
-
relativePath,
|
|
6211
|
-
newBody
|
|
6212
|
-
});
|
|
6213
|
-
return true;
|
|
6214
|
-
}
|
|
6215
|
-
|
|
6216
6432
|
// src/error.ts
|
|
6217
6433
|
import { GraphQLError as GraphQLError3 } from "graphql";
|
|
6218
6434
|
var NotFoundError = class extends GraphQLError3 {
|
|
@@ -6231,7 +6447,8 @@ var resolve = async ({
|
|
|
6231
6447
|
silenceErrors,
|
|
6232
6448
|
verbose,
|
|
6233
6449
|
isAudit,
|
|
6234
|
-
ctxUser
|
|
6450
|
+
ctxUser,
|
|
6451
|
+
isSignIn
|
|
6235
6452
|
}) => {
|
|
6236
6453
|
try {
|
|
6237
6454
|
const verboseValue = verbose ?? true;
|
|
@@ -6250,7 +6467,8 @@ var resolve = async ({
|
|
|
6250
6467
|
config,
|
|
6251
6468
|
database,
|
|
6252
6469
|
tinaSchema,
|
|
6253
|
-
isAudit: isAudit || false
|
|
6470
|
+
isAudit: isAudit || false,
|
|
6471
|
+
ctxUser
|
|
6254
6472
|
});
|
|
6255
6473
|
const resolverPromises = [];
|
|
6256
6474
|
const res = await graphql({
|
|
@@ -6294,7 +6512,7 @@ var resolve = async ({
|
|
|
6294
6512
|
sub: authArgs.sub,
|
|
6295
6513
|
password: authArgs.password,
|
|
6296
6514
|
info,
|
|
6297
|
-
|
|
6515
|
+
isSignIn
|
|
6298
6516
|
});
|
|
6299
6517
|
}
|
|
6300
6518
|
if (info.fieldName === "authorize") {
|
|
@@ -6660,7 +6878,11 @@ import path5 from "node:path";
|
|
|
6660
6878
|
import { GraphQLError as GraphQLError5 } from "graphql";
|
|
6661
6879
|
import micromatch2 from "micromatch";
|
|
6662
6880
|
import sha2 from "js-sha1";
|
|
6663
|
-
import { set
|
|
6881
|
+
import { set } from "es-toolkit/compat";
|
|
6882
|
+
var withUnauthenticatedWrites = (options) => ({
|
|
6883
|
+
...options,
|
|
6884
|
+
allowUnauthenticatedWrites: options?.allowUnauthenticatedWrites ?? true
|
|
6885
|
+
});
|
|
6664
6886
|
var createLocalDatabase = (config) => {
|
|
6665
6887
|
const level = new TinaLevelClient(config?.port);
|
|
6666
6888
|
level.openConnection();
|
|
@@ -6668,7 +6890,8 @@ var createLocalDatabase = (config) => {
|
|
|
6668
6890
|
return new Database({
|
|
6669
6891
|
bridge: fsBridge,
|
|
6670
6892
|
...config || {},
|
|
6671
|
-
level
|
|
6893
|
+
level,
|
|
6894
|
+
authCollection: withUnauthenticatedWrites(config?.authCollection)
|
|
6672
6895
|
});
|
|
6673
6896
|
};
|
|
6674
6897
|
var createDatabase = (config) => {
|
|
@@ -6709,7 +6932,8 @@ var createDatabaseInternal = (config) => {
|
|
|
6709
6932
|
return new Database({
|
|
6710
6933
|
...config,
|
|
6711
6934
|
bridge: config.bridge,
|
|
6712
|
-
level: config.level
|
|
6935
|
+
level: config.level,
|
|
6936
|
+
authCollection: withUnauthenticatedWrites(config.authCollection)
|
|
6713
6937
|
});
|
|
6714
6938
|
};
|
|
6715
6939
|
var SYSTEM_FILES = ["_schema", "_graphql", "_lookup"];
|
|
@@ -6728,6 +6952,10 @@ var Database = class {
|
|
|
6728
6952
|
this.onDelete = config.onDelete || defaultOnDelete;
|
|
6729
6953
|
this.levelBatchSize = config.levelBatchSize ?? DEFAULT_LEVEL_BATCH_SIZE;
|
|
6730
6954
|
this.contentNamespace = config.namespace;
|
|
6955
|
+
this.authCollection = Object.freeze({
|
|
6956
|
+
admins: Object.freeze([...config.authCollection?.admins ?? []]),
|
|
6957
|
+
allowUnauthenticatedWrites: config.authCollection?.allowUnauthenticatedWrites === true
|
|
6958
|
+
});
|
|
6731
6959
|
}
|
|
6732
6960
|
config;
|
|
6733
6961
|
bridge;
|
|
@@ -6739,6 +6967,7 @@ var Database = class {
|
|
|
6739
6967
|
onPut;
|
|
6740
6968
|
onDelete;
|
|
6741
6969
|
levelBatchSize;
|
|
6970
|
+
authCollection;
|
|
6742
6971
|
tinaSchema;
|
|
6743
6972
|
contentNamespace;
|
|
6744
6973
|
collectionIndexDefinitions;
|
|
@@ -7335,7 +7564,7 @@ var Database = class {
|
|
|
7335
7564
|
}
|
|
7336
7565
|
if (fields) {
|
|
7337
7566
|
for (const field of fields) {
|
|
7338
|
-
if (field.indexed !== void 0 && field.indexed === false || field.type === "object") {
|
|
7567
|
+
if (field.indexed !== void 0 && field.indexed === false || field.type === "password" || field.type === "object") {
|
|
7339
7568
|
continue;
|
|
7340
7569
|
}
|
|
7341
7570
|
indexDefinitions[field.name] = {
|
|
@@ -7800,7 +8029,7 @@ var hashPasswordVisitor = async (node, path9) => {
|
|
|
7800
8029
|
const passwordValuePath = [...path9, "value"];
|
|
7801
8030
|
const plaintextPassword = get(node, passwordValuePath);
|
|
7802
8031
|
if (plaintextPassword) {
|
|
7803
|
-
|
|
8032
|
+
set(
|
|
7804
8033
|
node,
|
|
7805
8034
|
passwordValuePath,
|
|
7806
8035
|
await generatePasswordHash({ password: plaintextPassword })
|
package/dist/resolve.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GraphQLConfig } from './types';
|
|
2
2
|
import type { Database } from './database';
|
|
3
|
-
|
|
3
|
+
import { type CtxUser } from './resolver';
|
|
4
|
+
export declare const resolve: ({ config, query, variables, database, silenceErrors, verbose, isAudit, ctxUser, isSignIn, }: {
|
|
4
5
|
config?: GraphQLConfig;
|
|
5
6
|
query: string;
|
|
6
7
|
variables: object;
|
|
@@ -8,9 +9,9 @@ export declare const resolve: ({ config, query, variables, database, silenceErro
|
|
|
8
9
|
silenceErrors?: boolean;
|
|
9
10
|
verbose?: boolean;
|
|
10
11
|
isAudit?: boolean;
|
|
11
|
-
ctxUser?:
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
ctxUser?: CtxUser;
|
|
13
|
+
/** Set only by the generated database client's `authenticate()`. */
|
|
14
|
+
isSignIn?: boolean;
|
|
14
15
|
}) => Promise<import("graphql").ExecutionResult<{
|
|
15
16
|
[key: string]: any;
|
|
16
17
|
}, {
|
|
@@ -1,15 +1,56 @@
|
|
|
1
|
-
import type { TinaSchema } from '@tinacms/schema-tools';
|
|
1
|
+
import type { Collection, TinaSchema } from '@tinacms/schema-tools';
|
|
2
2
|
import type { GraphQLResolveInfo } from 'graphql';
|
|
3
3
|
import type { Resolver } from './index';
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function getUserDocumentContext(tinaSchema: TinaSchema, resolver: Resolver): Promise<{
|
|
5
|
+
collection: Collection<true>;
|
|
6
|
+
userField: {
|
|
7
|
+
path: string[];
|
|
8
|
+
collectable: import("@tinacms/schema-tools").Collectable;
|
|
9
|
+
idFieldName?: string;
|
|
10
|
+
passwordFieldName?: string;
|
|
11
|
+
};
|
|
12
|
+
users: any;
|
|
13
|
+
userDoc: {
|
|
14
|
+
_sys: {
|
|
15
|
+
title: any;
|
|
16
|
+
basename: string;
|
|
17
|
+
filename: string;
|
|
18
|
+
extension: string;
|
|
19
|
+
hasReferences: boolean;
|
|
20
|
+
path: string;
|
|
21
|
+
relativePath: string;
|
|
22
|
+
breadcrumbs: string[];
|
|
23
|
+
collection: Collection<true>;
|
|
24
|
+
template: string | number;
|
|
25
|
+
};
|
|
26
|
+
_values: {
|
|
27
|
+
_collection: any;
|
|
28
|
+
_template: any;
|
|
29
|
+
};
|
|
30
|
+
_rawData: {
|
|
31
|
+
_collection: any;
|
|
32
|
+
_template: any;
|
|
33
|
+
};
|
|
34
|
+
_collection: any;
|
|
35
|
+
_template: any;
|
|
36
|
+
__typename: string;
|
|
37
|
+
id: string;
|
|
38
|
+
};
|
|
39
|
+
relativePath: string;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function findUserInCollection(users: any[], userField: any, userSub: string): any;
|
|
42
|
+
/**
|
|
43
|
+
* A stored hash only carries over to a user with the same uid, so a new or
|
|
44
|
+
* renamed user saved without a password could never sign in.
|
|
45
|
+
*/
|
|
46
|
+
export declare function assertNewUsersHavePasswords(collection: Collection<true>, newBody: Record<string, unknown>, existingData?: Record<string, unknown>): void;
|
|
47
|
+
export declare function handleAuthenticate({ tinaSchema, resolver, sub, password, isSignIn, }: {
|
|
5
48
|
tinaSchema: TinaSchema;
|
|
6
49
|
resolver: Resolver;
|
|
7
50
|
sub?: string;
|
|
8
51
|
password: string;
|
|
9
52
|
info: GraphQLResolveInfo;
|
|
10
|
-
|
|
11
|
-
sub?: string;
|
|
12
|
-
};
|
|
53
|
+
isSignIn?: boolean;
|
|
13
54
|
}): Promise<any>;
|
|
14
55
|
export declare function handleAuthorize({ tinaSchema, resolver, sub, ctxUser, }: {
|
|
15
56
|
tinaSchema: TinaSchema;
|
|
@@ -18,7 +59,7 @@ export declare function handleAuthorize({ tinaSchema, resolver, sub, ctxUser, }:
|
|
|
18
59
|
info: GraphQLResolveInfo;
|
|
19
60
|
ctxUser?: {
|
|
20
61
|
sub?: string;
|
|
21
|
-
};
|
|
62
|
+
} | null;
|
|
22
63
|
}): Promise<any>;
|
|
23
64
|
export declare function handleUpdatePassword({ tinaSchema, resolver, password, ctxUser, }: {
|
|
24
65
|
tinaSchema: TinaSchema;
|
|
@@ -27,5 +68,5 @@ export declare function handleUpdatePassword({ tinaSchema, resolver, password, c
|
|
|
27
68
|
info: GraphQLResolveInfo;
|
|
28
69
|
ctxUser?: {
|
|
29
70
|
sub?: string;
|
|
30
|
-
};
|
|
71
|
+
} | null;
|
|
31
72
|
}): Promise<boolean>;
|
package/dist/resolver/index.d.ts
CHANGED
|
@@ -4,11 +4,26 @@
|
|
|
4
4
|
import { Database } from '../database';
|
|
5
5
|
import type { Collectable, Collection, Template, TinaField, TinaSchema } from '@tinacms/schema-tools';
|
|
6
6
|
import type { GraphQLConfig } from '../types';
|
|
7
|
+
export type AuthCollectionWriteOp = 'create' | 'update' | 'rename' | 'delete' | 'addPending' | 'createFolder';
|
|
8
|
+
/**
|
|
9
|
+
* The signed-in user behind a request. Leave it undefined only when the caller
|
|
10
|
+
* vouches for the call itself, and pass null when nobody is signed in.
|
|
11
|
+
*/
|
|
12
|
+
export type CtxUser = {
|
|
13
|
+
sub?: string;
|
|
14
|
+
} | null | undefined;
|
|
15
|
+
type UpdateDocumentArgs = {
|
|
16
|
+
collectionName: string;
|
|
17
|
+
relativePath: string;
|
|
18
|
+
newRelativePath?: string;
|
|
19
|
+
newBody?: Record<string, unknown>;
|
|
20
|
+
};
|
|
7
21
|
interface ResolverConfig {
|
|
8
22
|
config?: GraphQLConfig;
|
|
9
23
|
database: Database;
|
|
10
24
|
tinaSchema: TinaSchema;
|
|
11
25
|
isAudit: boolean;
|
|
26
|
+
ctxUser?: CtxUser;
|
|
12
27
|
}
|
|
13
28
|
export declare const createResolver: (args: ResolverConfig) => Resolver;
|
|
14
29
|
export declare const resolveFieldData: ({ namespace, ...field }: TinaField<true>, rawData: unknown, accumulator: {
|
|
@@ -66,6 +81,7 @@ export declare class Resolver {
|
|
|
66
81
|
database: Database;
|
|
67
82
|
tinaSchema: TinaSchema;
|
|
68
83
|
isAudit: boolean;
|
|
84
|
+
ctxUser: CtxUser;
|
|
69
85
|
constructor(init: ResolverConfig);
|
|
70
86
|
resolveCollection: (_args: any, collectionName: string, hasDocuments?: boolean) => Promise<{
|
|
71
87
|
fields: TinaField<true>[];
|
|
@@ -248,6 +264,12 @@ export declare class Resolver {
|
|
|
248
264
|
*/
|
|
249
265
|
private static validateRelativePath;
|
|
250
266
|
private validatePath;
|
|
267
|
+
private assertAuthCollectionWrite;
|
|
268
|
+
private assertReferenceUpdates;
|
|
269
|
+
private isAuthCollectionPath;
|
|
270
|
+
private withAuthCollectionLock;
|
|
271
|
+
private isStoredInCollection;
|
|
272
|
+
private isStoredUser;
|
|
251
273
|
/**
|
|
252
274
|
* Helper method to get collection and construct validated path.
|
|
253
275
|
* This encapsulates the common pattern of getting a collection, joining paths,
|
|
@@ -256,7 +278,9 @@ export declare class Resolver {
|
|
|
256
278
|
* @param collectionName - Name of the collection
|
|
257
279
|
* @param relativePath - Relative path within the collection
|
|
258
280
|
* @param options - Optional configuration
|
|
259
|
-
* @returns Object containing the collection and validated real path
|
|
281
|
+
* @returns Object containing the collection and the validated real path,
|
|
282
|
+
* normalized to POSIX separators so it matches the document identities and
|
|
283
|
+
* reference values stored in content files on every platform
|
|
260
284
|
*/
|
|
261
285
|
private getValidatedPath;
|
|
262
286
|
resolveRetrievedDocument: ({ collectionName, relativePath, }: {
|
|
@@ -347,12 +371,7 @@ export declare class Resolver {
|
|
|
347
371
|
__typename: string;
|
|
348
372
|
id: string;
|
|
349
373
|
}>;
|
|
350
|
-
resolveUpdateDocument: (
|
|
351
|
-
collectionName: string;
|
|
352
|
-
relativePath: string;
|
|
353
|
-
newRelativePath?: string;
|
|
354
|
-
newBody?: Record<string, unknown>;
|
|
355
|
-
}) => Promise<{
|
|
374
|
+
resolveUpdateDocument: (args: UpdateDocumentArgs) => Promise<{
|
|
356
375
|
_sys: {
|
|
357
376
|
title: any;
|
|
358
377
|
basename: string;
|
|
@@ -378,6 +397,12 @@ export declare class Resolver {
|
|
|
378
397
|
__typename: string;
|
|
379
398
|
id: string;
|
|
380
399
|
}>;
|
|
400
|
+
private updateDocument;
|
|
401
|
+
/**
|
|
402
|
+
* Rewrites the auth collection's user store without the admin check, so the
|
|
403
|
+
* caller must check the request user.
|
|
404
|
+
*/
|
|
405
|
+
updateAuthDocumentInternal: (update: (rawData: Record<string, any>) => Record<string, any>) => Promise<void>;
|
|
381
406
|
resolveDeleteDocument: ({ collectionName, relativePath, }: {
|
|
382
407
|
collectionName: string;
|
|
383
408
|
relativePath: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinacms/graphql",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"files": [
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"normalize-path": "^3.0.0",
|
|
44
44
|
"readable-stream": "^4.7.0",
|
|
45
45
|
"yup": "^1.6.1",
|
|
46
|
-
"@tinacms/
|
|
47
|
-
"@tinacms/
|
|
46
|
+
"@tinacms/schema-tools": "^2.10.1",
|
|
47
|
+
"@tinacms/mdx": "^2.3.0"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"registry": "https://registry.npmjs.org"
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"vite": "^4.5.9",
|
|
73
73
|
"vitest": "^0.32.4",
|
|
74
74
|
"zod": "^3.24.2",
|
|
75
|
-
"@tinacms/schema-tools": "2.10.
|
|
75
|
+
"@tinacms/schema-tools": "2.10.1",
|
|
76
76
|
"@tinacms/scripts": "1.6.4"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|