@tinacms/graphql 3.0.0 → 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/database/index.d.ts +18 -0
- package/dist/index.js +408 -191
- package/dist/resolve.d.ts +5 -4
- package/dist/resolver/auth-fields.d.ts +48 -7
- package/dist/resolver/index.d.ts +29 -6
- 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
|
@@ -3058,7 +3058,7 @@ var assertCheckboxGroupIsList = (field) => {
|
|
|
3058
3058
|
var package_default = {
|
|
3059
3059
|
name: "@tinacms/graphql",
|
|
3060
3060
|
type: "module",
|
|
3061
|
-
version: "
|
|
3061
|
+
version: "4.0.0",
|
|
3062
3062
|
main: "dist/index.js",
|
|
3063
3063
|
module: "./dist/index.js",
|
|
3064
3064
|
files: [
|
|
@@ -3348,7 +3348,7 @@ import {
|
|
|
3348
3348
|
} from "graphql";
|
|
3349
3349
|
|
|
3350
3350
|
// src/resolver/index.ts
|
|
3351
|
-
import
|
|
3351
|
+
import path4 from "path";
|
|
3352
3352
|
import { isValid } from "date-fns";
|
|
3353
3353
|
import { JSONPath as JSONPath2 } from "jsonpath-plus";
|
|
3354
3354
|
|
|
@@ -4683,6 +4683,170 @@ var collectConditionsForField = (fieldName, field, filterNode, pathExpression, c
|
|
|
4683
4683
|
}
|
|
4684
4684
|
};
|
|
4685
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
|
+
|
|
4686
4850
|
// src/resolver/media-utils.ts
|
|
4687
4851
|
var resolveMediaCloudToRelative = (value, config = { useRelativeMedia: true }, schema) => {
|
|
4688
4852
|
if (config && value) {
|
|
@@ -4764,6 +4928,7 @@ var hasTinaMediaConfig = (schema) => {
|
|
|
4764
4928
|
};
|
|
4765
4929
|
|
|
4766
4930
|
// src/resolver/index.ts
|
|
4931
|
+
var authCollectionWrites = /* @__PURE__ */ new WeakMap();
|
|
4767
4932
|
var createResolver = (args) => {
|
|
4768
4933
|
return new Resolver(args);
|
|
4769
4934
|
};
|
|
@@ -4903,7 +5068,7 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
|
|
|
4903
5068
|
base: basename,
|
|
4904
5069
|
ext: extension,
|
|
4905
5070
|
name: filename
|
|
4906
|
-
} =
|
|
5071
|
+
} = path4.parse(normalizedPath);
|
|
4907
5072
|
const relativePath = normalizedPath.replace(collection.path, "").replace(/^\/|\/$/g, "");
|
|
4908
5073
|
const breadcrumbs = relativePath.replace(extension, "").split("/");
|
|
4909
5074
|
const data = {
|
|
@@ -5000,12 +5165,14 @@ var Resolver = class _Resolver {
|
|
|
5000
5165
|
this.database = init.database;
|
|
5001
5166
|
this.tinaSchema = init.tinaSchema;
|
|
5002
5167
|
this.isAudit = init.isAudit;
|
|
5168
|
+
this.ctxUser = init.ctxUser;
|
|
5003
5169
|
}
|
|
5004
5170
|
init;
|
|
5005
5171
|
config;
|
|
5006
5172
|
database;
|
|
5007
5173
|
tinaSchema;
|
|
5008
5174
|
isAudit;
|
|
5175
|
+
ctxUser;
|
|
5009
5176
|
resolveCollection = async (_args, collectionName, hasDocuments) => {
|
|
5010
5177
|
const collection = this.tinaSchema.getCollection(collectionName);
|
|
5011
5178
|
const extraFields = {};
|
|
@@ -5182,6 +5349,11 @@ var Resolver = class _Resolver {
|
|
|
5182
5349
|
collectionName,
|
|
5183
5350
|
relativePath
|
|
5184
5351
|
);
|
|
5352
|
+
await this.assertAuthCollectionWrite({
|
|
5353
|
+
collection,
|
|
5354
|
+
realPath,
|
|
5355
|
+
op: "addPending"
|
|
5356
|
+
});
|
|
5185
5357
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5186
5358
|
if (alreadyExists) {
|
|
5187
5359
|
throw new Error(
|
|
@@ -5297,19 +5469,19 @@ var Resolver = class _Resolver {
|
|
|
5297
5469
|
if (fullPath.includes("\0")) {
|
|
5298
5470
|
throw new Error("Invalid path: null bytes are not allowed");
|
|
5299
5471
|
}
|
|
5300
|
-
const normalizedPath =
|
|
5301
|
-
const normalizedCollectionPath =
|
|
5302
|
-
const relative =
|
|
5472
|
+
const normalizedPath = path4.normalize(fullPath.replace(/\\/g, "/"));
|
|
5473
|
+
const normalizedCollectionPath = path4.normalize(collection.path);
|
|
5474
|
+
const relative = path4.relative(normalizedCollectionPath, normalizedPath);
|
|
5303
5475
|
if (relative.startsWith("..")) {
|
|
5304
5476
|
throw new Error(`Invalid path: path escapes the collection directory`);
|
|
5305
5477
|
}
|
|
5306
|
-
if (
|
|
5478
|
+
if (path4.isAbsolute(relative)) {
|
|
5307
5479
|
throw new Error(`Invalid path: absolute paths are not allowed`);
|
|
5308
5480
|
}
|
|
5309
5481
|
if (relativePath) {
|
|
5310
5482
|
_Resolver.validateRelativePath(relativePath);
|
|
5311
5483
|
const collectionFormat = collection.format || "md";
|
|
5312
|
-
const fileExtension =
|
|
5484
|
+
const fileExtension = path4.extname(relativePath).toLowerCase().slice(1);
|
|
5313
5485
|
if (fileExtension !== collectionFormat) {
|
|
5314
5486
|
throw new Error(
|
|
5315
5487
|
`Invalid file extension: expected '.${collectionFormat}' but got '.${fileExtension}'`
|
|
@@ -5317,6 +5489,81 @@ var Resolver = class _Resolver {
|
|
|
5317
5489
|
}
|
|
5318
5490
|
}
|
|
5319
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
|
+
};
|
|
5320
5567
|
/**
|
|
5321
5568
|
* Helper method to get collection and construct validated path.
|
|
5322
5569
|
* This encapsulates the common pattern of getting a collection, joining paths,
|
|
@@ -5337,7 +5584,7 @@ var Resolver = class _Resolver {
|
|
|
5337
5584
|
if (options?.extraSegments) {
|
|
5338
5585
|
pathSegments.push(...options.extraSegments.map(_Resolver.sanitizePath));
|
|
5339
5586
|
}
|
|
5340
|
-
const realPath = normalizePath2(
|
|
5587
|
+
const realPath = normalizePath2(path4.join(...pathSegments));
|
|
5341
5588
|
const shouldValidateExtension = options?.validateExtension !== false;
|
|
5342
5589
|
this.validatePath(
|
|
5343
5590
|
realPath,
|
|
@@ -5371,12 +5618,17 @@ var Resolver = class _Resolver {
|
|
|
5371
5618
|
}) => {
|
|
5372
5619
|
const collection = this.getCollectionWithName(collectionName);
|
|
5373
5620
|
_Resolver.validateRelativePath(relativePath);
|
|
5374
|
-
const realPath =
|
|
5621
|
+
const realPath = path4.join(
|
|
5375
5622
|
collection.path,
|
|
5376
5623
|
relativePath,
|
|
5377
5624
|
`.gitkeep.${collection.format || "md"}`
|
|
5378
5625
|
);
|
|
5379
5626
|
this.validatePath(realPath, collection);
|
|
5627
|
+
await this.assertAuthCollectionWrite({
|
|
5628
|
+
collection,
|
|
5629
|
+
realPath,
|
|
5630
|
+
op: "createFolder"
|
|
5631
|
+
});
|
|
5380
5632
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5381
5633
|
if (alreadyExists) {
|
|
5382
5634
|
throw new Error(
|
|
@@ -5399,6 +5651,11 @@ var Resolver = class _Resolver {
|
|
|
5399
5651
|
collectionName,
|
|
5400
5652
|
relativePath
|
|
5401
5653
|
);
|
|
5654
|
+
await this.assertAuthCollectionWrite({
|
|
5655
|
+
collection,
|
|
5656
|
+
realPath,
|
|
5657
|
+
op: "create"
|
|
5658
|
+
});
|
|
5402
5659
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5403
5660
|
if (alreadyExists) {
|
|
5404
5661
|
throw new Error(
|
|
@@ -5409,7 +5666,17 @@ var Resolver = class _Resolver {
|
|
|
5409
5666
|
await this.database.put(realPath, params, collection.name);
|
|
5410
5667
|
return this.getDocument(realPath);
|
|
5411
5668
|
};
|
|
5412
|
-
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 ({
|
|
5413
5680
|
collectionName,
|
|
5414
5681
|
relativePath,
|
|
5415
5682
|
newRelativePath,
|
|
@@ -5419,6 +5686,13 @@ var Resolver = class _Resolver {
|
|
|
5419
5686
|
collectionName,
|
|
5420
5687
|
relativePath
|
|
5421
5688
|
);
|
|
5689
|
+
if (!newRelativePath) {
|
|
5690
|
+
await this.assertAuthCollectionWrite({
|
|
5691
|
+
collection,
|
|
5692
|
+
realPath,
|
|
5693
|
+
op: "update"
|
|
5694
|
+
});
|
|
5695
|
+
}
|
|
5422
5696
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5423
5697
|
if (!alreadyExists) {
|
|
5424
5698
|
throw new Error(`Unable to update document, ${realPath} does not exist`);
|
|
@@ -5438,38 +5712,53 @@ var Resolver = class _Resolver {
|
|
|
5438
5712
|
`Unable to rename document, ${newRealPath} ${ERR_ALREADY_EXISTS}`
|
|
5439
5713
|
);
|
|
5440
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);
|
|
5441
5726
|
await this.database.put(newRealPath, doc._rawData, collection.name);
|
|
5442
5727
|
await this.deleteDocument(realPath);
|
|
5443
5728
|
const collRefs = await this.findReferences(realPath, collection);
|
|
5729
|
+
const holdsLock = this.isAuthCollectionPath(realPath);
|
|
5444
5730
|
for (const [_collection, docsWithRefs] of Object.entries(collRefs)) {
|
|
5445
5731
|
for (const [pathToDocWithRef, referencePaths] of Object.entries(
|
|
5446
5732
|
docsWithRefs
|
|
5447
5733
|
)) {
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
const
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
docWithRef = object2;
|
|
5458
|
-
hasUpdate = updated || hasUpdate;
|
|
5459
|
-
}
|
|
5460
|
-
if (hasUpdate) {
|
|
5461
|
-
const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
|
|
5462
|
-
if (!collectionWithRef) {
|
|
5463
|
-
throw new Error(
|
|
5464
|
-
`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
|
|
5465
5743
|
);
|
|
5744
|
+
docWithRef = object2;
|
|
5745
|
+
hasUpdate = updated || hasUpdate;
|
|
5466
5746
|
}
|
|
5467
|
-
|
|
5468
|
-
pathToDocWithRef
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
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));
|
|
5473
5762
|
}
|
|
5474
5763
|
}
|
|
5475
5764
|
return this.getDocument(newRealPath);
|
|
@@ -5477,6 +5766,9 @@ var Resolver = class _Resolver {
|
|
|
5477
5766
|
if (!newBody) {
|
|
5478
5767
|
throw new Error("Body not provided for updated document.");
|
|
5479
5768
|
}
|
|
5769
|
+
if (collection.isAuthCollection) {
|
|
5770
|
+
assertNewUsersHavePasswords(collection, newBody, doc?._rawData);
|
|
5771
|
+
}
|
|
5480
5772
|
const params = await this.buildObjectMutations(
|
|
5481
5773
|
newBody,
|
|
5482
5774
|
collection,
|
|
@@ -5493,6 +5785,21 @@ var Resolver = class _Resolver {
|
|
|
5493
5785
|
);
|
|
5494
5786
|
return this.getDocument(realPath);
|
|
5495
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
|
+
};
|
|
5496
5803
|
resolveDeleteDocument = async ({
|
|
5497
5804
|
collectionName,
|
|
5498
5805
|
relativePath
|
|
@@ -5505,39 +5812,50 @@ var Resolver = class _Resolver {
|
|
|
5505
5812
|
if (!alreadyExists) {
|
|
5506
5813
|
throw new Error(`Unable to delete document, ${realPath} does not exist`);
|
|
5507
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
|
+
}
|
|
5508
5824
|
const doc = await this.getDocument(realPath);
|
|
5509
5825
|
await this.deleteDocument(realPath);
|
|
5510
|
-
if (
|
|
5826
|
+
if (hasReferences) {
|
|
5511
5827
|
const collRefs = await this.findReferences(realPath, collection);
|
|
5512
5828
|
for (const [_collection, docsWithRefs] of Object.entries(collRefs)) {
|
|
5513
5829
|
for (const [pathToDocWithRef, referencePaths] of Object.entries(
|
|
5514
5830
|
docsWithRefs
|
|
5515
5831
|
)) {
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
const
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
refDoc = object2;
|
|
5526
|
-
hasUpdate = updated || hasUpdate;
|
|
5527
|
-
}
|
|
5528
|
-
if (hasUpdate) {
|
|
5529
|
-
const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
|
|
5530
|
-
if (!collectionWithRef) {
|
|
5531
|
-
throw new Error(
|
|
5532
|
-
`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
|
|
5533
5841
|
);
|
|
5842
|
+
refDoc = object2;
|
|
5843
|
+
hasUpdate = updated || hasUpdate;
|
|
5534
5844
|
}
|
|
5535
|
-
|
|
5536
|
-
pathToDocWithRef
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
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
|
+
});
|
|
5541
5859
|
}
|
|
5542
5860
|
}
|
|
5543
5861
|
}
|
|
@@ -5573,10 +5891,15 @@ var Resolver = class _Resolver {
|
|
|
5573
5891
|
if (isAddPendingDocument === true) {
|
|
5574
5892
|
return this.resolveAddPendingDocument({
|
|
5575
5893
|
collectionName: collection.name,
|
|
5576
|
-
relativePath:
|
|
5894
|
+
relativePath: path4.relative(collection.path, realPath),
|
|
5577
5895
|
templateName: args.template
|
|
5578
5896
|
});
|
|
5579
5897
|
}
|
|
5898
|
+
await this.assertAuthCollectionWrite({
|
|
5899
|
+
collection,
|
|
5900
|
+
realPath,
|
|
5901
|
+
op: "create"
|
|
5902
|
+
});
|
|
5580
5903
|
const params = await this.buildObjectMutations(
|
|
5581
5904
|
// @ts-ignore
|
|
5582
5905
|
args.params[collection.name],
|
|
@@ -5595,6 +5918,11 @@ var Resolver = class _Resolver {
|
|
|
5595
5918
|
isAddPendingDocument,
|
|
5596
5919
|
isCollectionSpecific
|
|
5597
5920
|
}) => {
|
|
5921
|
+
await this.assertAuthCollectionWrite({
|
|
5922
|
+
collection,
|
|
5923
|
+
realPath,
|
|
5924
|
+
op: "update"
|
|
5925
|
+
});
|
|
5598
5926
|
const doc = await this.getDocument(realPath);
|
|
5599
5927
|
const oldDoc = this.resolveLegacyValues(doc?._rawData || {}, collection);
|
|
5600
5928
|
if (isAddPendingDocument === true) {
|
|
@@ -6101,130 +6429,6 @@ var resolveDateInput = (value) => {
|
|
|
6101
6429
|
return date;
|
|
6102
6430
|
};
|
|
6103
6431
|
|
|
6104
|
-
// src/resolver/auth-fields.ts
|
|
6105
|
-
import path4 from "path";
|
|
6106
|
-
import { set } from "es-toolkit/compat";
|
|
6107
|
-
async function getUserDocumentContext(tinaSchema, resolver) {
|
|
6108
|
-
const collection = tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
6109
|
-
if (!collection) {
|
|
6110
|
-
throw new Error("Auth collection not found");
|
|
6111
|
-
}
|
|
6112
|
-
const userFields = mapUserFields(collection, ["_rawData"]);
|
|
6113
|
-
if (!userFields.length) {
|
|
6114
|
-
throw new Error(`No user field found in collection ${collection.name}`);
|
|
6115
|
-
}
|
|
6116
|
-
if (userFields.length > 1) {
|
|
6117
|
-
throw new Error(
|
|
6118
|
-
`Multiple user fields found in collection ${collection.name}`
|
|
6119
|
-
);
|
|
6120
|
-
}
|
|
6121
|
-
const userField = userFields[0];
|
|
6122
|
-
const relativePath = "index.json";
|
|
6123
|
-
const realPath = path4.join(collection.path, relativePath);
|
|
6124
|
-
const userDoc = await resolver.getDocument(realPath);
|
|
6125
|
-
const users = get(userDoc, userField.path);
|
|
6126
|
-
if (!users) {
|
|
6127
|
-
throw new Error("No users found");
|
|
6128
|
-
}
|
|
6129
|
-
return { collection, userField, users, userDoc, relativePath };
|
|
6130
|
-
}
|
|
6131
|
-
function findUserInCollection(users, userField, userSub) {
|
|
6132
|
-
const { idFieldName } = userField;
|
|
6133
|
-
if (!idFieldName) {
|
|
6134
|
-
throw new Error("No uid field found on user field");
|
|
6135
|
-
}
|
|
6136
|
-
return users.find((u) => u[idFieldName] === userSub) || null;
|
|
6137
|
-
}
|
|
6138
|
-
async function handleAuthenticate({
|
|
6139
|
-
tinaSchema,
|
|
6140
|
-
resolver,
|
|
6141
|
-
sub,
|
|
6142
|
-
password,
|
|
6143
|
-
ctxUser
|
|
6144
|
-
}) {
|
|
6145
|
-
const userSub = sub || ctxUser?.sub;
|
|
6146
|
-
const { userField, users } = await getUserDocumentContext(
|
|
6147
|
-
tinaSchema,
|
|
6148
|
-
resolver
|
|
6149
|
-
);
|
|
6150
|
-
const user = findUserInCollection(users, userField, userSub);
|
|
6151
|
-
if (!user) {
|
|
6152
|
-
return null;
|
|
6153
|
-
}
|
|
6154
|
-
const { passwordFieldName } = userField;
|
|
6155
|
-
const saltedHash = get(user, [passwordFieldName || "", "value"]);
|
|
6156
|
-
if (!saltedHash) {
|
|
6157
|
-
throw new Error("No password field found on user field");
|
|
6158
|
-
}
|
|
6159
|
-
const matches = await checkPasswordHash({
|
|
6160
|
-
saltedHash,
|
|
6161
|
-
password
|
|
6162
|
-
});
|
|
6163
|
-
return matches ? user : null;
|
|
6164
|
-
}
|
|
6165
|
-
async function handleAuthorize({
|
|
6166
|
-
tinaSchema,
|
|
6167
|
-
resolver,
|
|
6168
|
-
sub,
|
|
6169
|
-
ctxUser
|
|
6170
|
-
}) {
|
|
6171
|
-
const userSub = sub || ctxUser?.sub;
|
|
6172
|
-
const { userField, users } = await getUserDocumentContext(
|
|
6173
|
-
tinaSchema,
|
|
6174
|
-
resolver
|
|
6175
|
-
);
|
|
6176
|
-
const user = findUserInCollection(users, userField, userSub);
|
|
6177
|
-
return user ? user : null;
|
|
6178
|
-
}
|
|
6179
|
-
async function handleUpdatePassword({
|
|
6180
|
-
tinaSchema,
|
|
6181
|
-
resolver,
|
|
6182
|
-
password,
|
|
6183
|
-
ctxUser
|
|
6184
|
-
}) {
|
|
6185
|
-
if (!ctxUser?.sub) {
|
|
6186
|
-
throw new Error("Not authorized");
|
|
6187
|
-
}
|
|
6188
|
-
if (!password) {
|
|
6189
|
-
throw new Error("No password provided");
|
|
6190
|
-
}
|
|
6191
|
-
const { collection, userField, users, relativePath } = await getUserDocumentContext(tinaSchema, resolver);
|
|
6192
|
-
const { idFieldName, passwordFieldName } = userField;
|
|
6193
|
-
const user = users.find((u) => u[idFieldName] === ctxUser.sub);
|
|
6194
|
-
if (!user) {
|
|
6195
|
-
throw new Error("Not authorized");
|
|
6196
|
-
}
|
|
6197
|
-
user[passwordFieldName] = {
|
|
6198
|
-
value: password,
|
|
6199
|
-
passwordChangeRequired: false
|
|
6200
|
-
};
|
|
6201
|
-
const newBody = {};
|
|
6202
|
-
set(
|
|
6203
|
-
newBody,
|
|
6204
|
-
userField.path.slice(1),
|
|
6205
|
-
// remove _rawData from users path
|
|
6206
|
-
users.map((u) => {
|
|
6207
|
-
if (user[idFieldName] === u[idFieldName]) {
|
|
6208
|
-
return user;
|
|
6209
|
-
}
|
|
6210
|
-
return {
|
|
6211
|
-
// don't overwrite other users' passwords
|
|
6212
|
-
...u,
|
|
6213
|
-
[passwordFieldName]: {
|
|
6214
|
-
...u[passwordFieldName],
|
|
6215
|
-
value: ""
|
|
6216
|
-
}
|
|
6217
|
-
};
|
|
6218
|
-
})
|
|
6219
|
-
);
|
|
6220
|
-
await resolver.resolveUpdateDocument({
|
|
6221
|
-
collectionName: collection.name,
|
|
6222
|
-
relativePath,
|
|
6223
|
-
newBody
|
|
6224
|
-
});
|
|
6225
|
-
return true;
|
|
6226
|
-
}
|
|
6227
|
-
|
|
6228
6432
|
// src/error.ts
|
|
6229
6433
|
import { GraphQLError as GraphQLError3 } from "graphql";
|
|
6230
6434
|
var NotFoundError = class extends GraphQLError3 {
|
|
@@ -6243,7 +6447,8 @@ var resolve = async ({
|
|
|
6243
6447
|
silenceErrors,
|
|
6244
6448
|
verbose,
|
|
6245
6449
|
isAudit,
|
|
6246
|
-
ctxUser
|
|
6450
|
+
ctxUser,
|
|
6451
|
+
isSignIn
|
|
6247
6452
|
}) => {
|
|
6248
6453
|
try {
|
|
6249
6454
|
const verboseValue = verbose ?? true;
|
|
@@ -6262,7 +6467,8 @@ var resolve = async ({
|
|
|
6262
6467
|
config,
|
|
6263
6468
|
database,
|
|
6264
6469
|
tinaSchema,
|
|
6265
|
-
isAudit: isAudit || false
|
|
6470
|
+
isAudit: isAudit || false,
|
|
6471
|
+
ctxUser
|
|
6266
6472
|
});
|
|
6267
6473
|
const resolverPromises = [];
|
|
6268
6474
|
const res = await graphql({
|
|
@@ -6306,7 +6512,7 @@ var resolve = async ({
|
|
|
6306
6512
|
sub: authArgs.sub,
|
|
6307
6513
|
password: authArgs.password,
|
|
6308
6514
|
info,
|
|
6309
|
-
|
|
6515
|
+
isSignIn
|
|
6310
6516
|
});
|
|
6311
6517
|
}
|
|
6312
6518
|
if (info.fieldName === "authorize") {
|
|
@@ -6672,7 +6878,11 @@ import path5 from "node:path";
|
|
|
6672
6878
|
import { GraphQLError as GraphQLError5 } from "graphql";
|
|
6673
6879
|
import micromatch2 from "micromatch";
|
|
6674
6880
|
import sha2 from "js-sha1";
|
|
6675
|
-
import { set
|
|
6881
|
+
import { set } from "es-toolkit/compat";
|
|
6882
|
+
var withUnauthenticatedWrites = (options) => ({
|
|
6883
|
+
...options,
|
|
6884
|
+
allowUnauthenticatedWrites: options?.allowUnauthenticatedWrites ?? true
|
|
6885
|
+
});
|
|
6676
6886
|
var createLocalDatabase = (config) => {
|
|
6677
6887
|
const level = new TinaLevelClient(config?.port);
|
|
6678
6888
|
level.openConnection();
|
|
@@ -6680,7 +6890,8 @@ var createLocalDatabase = (config) => {
|
|
|
6680
6890
|
return new Database({
|
|
6681
6891
|
bridge: fsBridge,
|
|
6682
6892
|
...config || {},
|
|
6683
|
-
level
|
|
6893
|
+
level,
|
|
6894
|
+
authCollection: withUnauthenticatedWrites(config?.authCollection)
|
|
6684
6895
|
});
|
|
6685
6896
|
};
|
|
6686
6897
|
var createDatabase = (config) => {
|
|
@@ -6721,7 +6932,8 @@ var createDatabaseInternal = (config) => {
|
|
|
6721
6932
|
return new Database({
|
|
6722
6933
|
...config,
|
|
6723
6934
|
bridge: config.bridge,
|
|
6724
|
-
level: config.level
|
|
6935
|
+
level: config.level,
|
|
6936
|
+
authCollection: withUnauthenticatedWrites(config.authCollection)
|
|
6725
6937
|
});
|
|
6726
6938
|
};
|
|
6727
6939
|
var SYSTEM_FILES = ["_schema", "_graphql", "_lookup"];
|
|
@@ -6740,6 +6952,10 @@ var Database = class {
|
|
|
6740
6952
|
this.onDelete = config.onDelete || defaultOnDelete;
|
|
6741
6953
|
this.levelBatchSize = config.levelBatchSize ?? DEFAULT_LEVEL_BATCH_SIZE;
|
|
6742
6954
|
this.contentNamespace = config.namespace;
|
|
6955
|
+
this.authCollection = Object.freeze({
|
|
6956
|
+
admins: Object.freeze([...config.authCollection?.admins ?? []]),
|
|
6957
|
+
allowUnauthenticatedWrites: config.authCollection?.allowUnauthenticatedWrites === true
|
|
6958
|
+
});
|
|
6743
6959
|
}
|
|
6744
6960
|
config;
|
|
6745
6961
|
bridge;
|
|
@@ -6751,6 +6967,7 @@ var Database = class {
|
|
|
6751
6967
|
onPut;
|
|
6752
6968
|
onDelete;
|
|
6753
6969
|
levelBatchSize;
|
|
6970
|
+
authCollection;
|
|
6754
6971
|
tinaSchema;
|
|
6755
6972
|
contentNamespace;
|
|
6756
6973
|
collectionIndexDefinitions;
|
|
@@ -7347,7 +7564,7 @@ var Database = class {
|
|
|
7347
7564
|
}
|
|
7348
7565
|
if (fields) {
|
|
7349
7566
|
for (const field of fields) {
|
|
7350
|
-
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") {
|
|
7351
7568
|
continue;
|
|
7352
7569
|
}
|
|
7353
7570
|
indexDefinitions[field.name] = {
|
|
@@ -7812,7 +8029,7 @@ var hashPasswordVisitor = async (node, path9) => {
|
|
|
7812
8029
|
const passwordValuePath = [...path9, "value"];
|
|
7813
8030
|
const plaintextPassword = get(node, passwordValuePath);
|
|
7814
8031
|
if (plaintextPassword) {
|
|
7815
|
-
|
|
8032
|
+
set(
|
|
7816
8033
|
node,
|
|
7817
8034
|
passwordValuePath,
|
|
7818
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,
|
|
@@ -349,12 +371,7 @@ export declare class Resolver {
|
|
|
349
371
|
__typename: string;
|
|
350
372
|
id: string;
|
|
351
373
|
}>;
|
|
352
|
-
resolveUpdateDocument: (
|
|
353
|
-
collectionName: string;
|
|
354
|
-
relativePath: string;
|
|
355
|
-
newRelativePath?: string;
|
|
356
|
-
newBody?: Record<string, unknown>;
|
|
357
|
-
}) => Promise<{
|
|
374
|
+
resolveUpdateDocument: (args: UpdateDocumentArgs) => Promise<{
|
|
358
375
|
_sys: {
|
|
359
376
|
title: any;
|
|
360
377
|
basename: string;
|
|
@@ -380,6 +397,12 @@ export declare class Resolver {
|
|
|
380
397
|
__typename: string;
|
|
381
398
|
id: string;
|
|
382
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>;
|
|
383
406
|
resolveDeleteDocument: ({ collectionName, relativePath, }: {
|
|
384
407
|
collectionName: string;
|
|
385
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": {
|