@codixus/server 0.1.2 → 0.1.4
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/index.cjs +393 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -4
- package/dist/index.d.ts +97 -4
- package/dist/index.js +392 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
ErrorCodes: () => import_shared4.ErrorCodes,
|
|
36
36
|
Model: () => Model,
|
|
37
37
|
Query: () => Query,
|
|
38
|
+
RestErrorCode: () => RestErrorCode,
|
|
38
39
|
SubCollection: () => SubCollection,
|
|
39
40
|
model: () => model,
|
|
40
41
|
validate: () => validate
|
|
@@ -180,7 +181,7 @@ var BanService = class {
|
|
|
180
181
|
};
|
|
181
182
|
|
|
182
183
|
// src/auth/guard.ts
|
|
183
|
-
function createGuard(jwt) {
|
|
184
|
+
function createGuard(jwt, banService) {
|
|
184
185
|
return function guard(options = {}) {
|
|
185
186
|
return async (req, res, next) => {
|
|
186
187
|
const authHeader = req.headers.authorization;
|
|
@@ -195,9 +196,14 @@ function createGuard(jwt) {
|
|
|
195
196
|
const token = authHeader.substring(7);
|
|
196
197
|
try {
|
|
197
198
|
const payload = await jwt.verify(token);
|
|
199
|
+
const deviceId = payload.deviceId;
|
|
200
|
+
if (banService && await banService.isBanned(deviceId)) {
|
|
201
|
+
res.status(403).json({ success: false, error: "DEVICE_BANNED" });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
198
204
|
req.user = {
|
|
199
205
|
uid: payload.sub,
|
|
200
|
-
deviceId
|
|
206
|
+
deviceId
|
|
201
207
|
};
|
|
202
208
|
next();
|
|
203
209
|
} catch (error) {
|
|
@@ -798,12 +804,12 @@ function createRateLimit(config = {}) {
|
|
|
798
804
|
}
|
|
799
805
|
entry.count++;
|
|
800
806
|
const remaining = Math.max(0, maxRequests - entry.count);
|
|
801
|
-
res.setHeader("X-RateLimit-Limit", maxRequests);
|
|
802
|
-
res.setHeader("X-RateLimit-Remaining", remaining);
|
|
803
|
-
res.setHeader("X-RateLimit-Reset", Math.ceil(entry.resetAt / 1e3));
|
|
807
|
+
res.setHeader("X-RateLimit-Limit", String(maxRequests));
|
|
808
|
+
res.setHeader("X-RateLimit-Remaining", String(remaining));
|
|
809
|
+
res.setHeader("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1e3)));
|
|
804
810
|
if (entry.count > maxRequests) {
|
|
805
811
|
const retryAfter = Math.ceil((entry.resetAt - now) / 1e3);
|
|
806
|
-
res.setHeader("Retry-After", retryAfter);
|
|
812
|
+
res.setHeader("Retry-After", String(retryAfter));
|
|
807
813
|
res.status(429).json({
|
|
808
814
|
success: false,
|
|
809
815
|
error: message,
|
|
@@ -823,6 +829,31 @@ function defaultKeyGenerator(req) {
|
|
|
823
829
|
// src/db/rest.ts
|
|
824
830
|
var import_express2 = require("express");
|
|
825
831
|
var import_node_crypto4 = __toESM(require("crypto"), 1);
|
|
832
|
+
var RestErrorCode = {
|
|
833
|
+
CreateDisabled: "CREATE_DISABLED",
|
|
834
|
+
UpdateDisabled: "UPDATE_DISABLED",
|
|
835
|
+
DeleteDisabled: "DELETE_DISABLED",
|
|
836
|
+
ReadForbidden: "READ_FORBIDDEN",
|
|
837
|
+
WriteForbidden: "WRITE_FORBIDDEN",
|
|
838
|
+
DeleteForbidden: "DELETE_FORBIDDEN",
|
|
839
|
+
NotFound: "NOT_FOUND",
|
|
840
|
+
InvalidFilter: "INVALID_FILTER",
|
|
841
|
+
InvalidSort: "INVALID_SORT",
|
|
842
|
+
InvalidCursor: "INVALID_CURSOR",
|
|
843
|
+
InvalidUpdate: "INVALID_UPDATE",
|
|
844
|
+
InvalidBody: "INVALID_BODY",
|
|
845
|
+
AuthRequired: "AUTH_REQUIRED"
|
|
846
|
+
};
|
|
847
|
+
var DEFAULT_ALLOWED_OPS = /* @__PURE__ */ new Set([
|
|
848
|
+
"$eq",
|
|
849
|
+
"$ne",
|
|
850
|
+
"$in",
|
|
851
|
+
"$nin",
|
|
852
|
+
"$gt",
|
|
853
|
+
"$gte",
|
|
854
|
+
"$lt",
|
|
855
|
+
"$lte"
|
|
856
|
+
]);
|
|
826
857
|
var SAFE_UPDATE_OPS = /* @__PURE__ */ new Set([
|
|
827
858
|
"$set",
|
|
828
859
|
"$unset",
|
|
@@ -837,41 +868,199 @@ var SAFE_UPDATE_OPS = /* @__PURE__ */ new Set([
|
|
|
837
868
|
"$currentDate",
|
|
838
869
|
"$setOnInsert"
|
|
839
870
|
]);
|
|
840
|
-
var
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
]
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
871
|
+
var RestError = class extends Error {
|
|
872
|
+
constructor(code, message) {
|
|
873
|
+
super(message);
|
|
874
|
+
this.code = code;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
function param(req, name) {
|
|
878
|
+
const val = req.params[name];
|
|
879
|
+
return Array.isArray(val) ? val[0] : val ?? "";
|
|
880
|
+
}
|
|
881
|
+
function fail(res, status, code, error) {
|
|
882
|
+
res.status(status).json({ success: false, error, code });
|
|
883
|
+
}
|
|
884
|
+
function handleCaught(res, err) {
|
|
885
|
+
if (err instanceof RestError) {
|
|
886
|
+
const status = err.code === RestErrorCode.AuthRequired ? 401 : 400;
|
|
887
|
+
fail(res, status, err.code, err.message);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
891
|
+
fail(res, 400, RestErrorCode.InvalidBody, message);
|
|
892
|
+
}
|
|
893
|
+
function buildFilterValidator(queryableFields, allowedOps) {
|
|
894
|
+
return function sanitizeFilter(obj, depth = 0) {
|
|
895
|
+
if (depth > 10)
|
|
896
|
+
throw new RestError(RestErrorCode.InvalidFilter, "Filter too deeply nested");
|
|
897
|
+
if (obj === null || obj === void 0) return {};
|
|
898
|
+
if (typeof obj !== "object" || Array.isArray(obj))
|
|
899
|
+
throw new RestError(RestErrorCode.InvalidFilter, "Filter must be an object");
|
|
900
|
+
const result = {};
|
|
901
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
902
|
+
if (key.startsWith("$")) {
|
|
903
|
+
if (!allowedOps.has(key)) {
|
|
904
|
+
throw new RestError(
|
|
905
|
+
RestErrorCode.InvalidFilter,
|
|
906
|
+
`Operator "${key}" is not allowed`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
result[key] = sanitizeFilterValue(value, allowedOps, depth + 1);
|
|
910
|
+
} else {
|
|
911
|
+
if (queryableFields && !queryableFields.has(key)) {
|
|
912
|
+
throw new RestError(
|
|
913
|
+
RestErrorCode.InvalidFilter,
|
|
914
|
+
`Field "${key}" is not queryable`
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
result[key] = sanitizeFilterValue(value, allowedOps, depth + 1);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return result;
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function sanitizeFilterValue(value, allowedOps, depth) {
|
|
924
|
+
if (depth > 10)
|
|
925
|
+
throw new RestError(RestErrorCode.InvalidFilter, "Filter too deeply nested");
|
|
926
|
+
if (value === null) return null;
|
|
927
|
+
if (Array.isArray(value)) {
|
|
928
|
+
return value.map((v) => sanitizeFilterValue(v, allowedOps, depth + 1));
|
|
929
|
+
}
|
|
930
|
+
if (value instanceof Date) return value;
|
|
931
|
+
if (typeof value !== "object") return value;
|
|
852
932
|
const result = {};
|
|
853
|
-
for (const [
|
|
854
|
-
if (
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
933
|
+
for (const [k, v] of Object.entries(value)) {
|
|
934
|
+
if (k.startsWith("$") && !allowedOps.has(k)) {
|
|
935
|
+
throw new RestError(
|
|
936
|
+
RestErrorCode.InvalidFilter,
|
|
937
|
+
`Operator "${k}" is not allowed`
|
|
938
|
+
);
|
|
859
939
|
}
|
|
940
|
+
result[k] = sanitizeFilterValue(v, allowedOps, depth + 1);
|
|
860
941
|
}
|
|
861
942
|
return result;
|
|
862
943
|
}
|
|
863
|
-
function
|
|
864
|
-
if (
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
944
|
+
function assertNoNestedOperators(value, path, depth = 0) {
|
|
945
|
+
if (depth > 10) return;
|
|
946
|
+
if (value === null || typeof value !== "object") return;
|
|
947
|
+
if (value instanceof Date) return;
|
|
948
|
+
if (Array.isArray(value)) {
|
|
949
|
+
for (const v of value) assertNoNestedOperators(v, path, depth + 1);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
for (const [k, v] of Object.entries(value)) {
|
|
953
|
+
if (k.startsWith("$")) {
|
|
954
|
+
throw new RestError(
|
|
955
|
+
RestErrorCode.InvalidUpdate,
|
|
956
|
+
`Nested operator "${k}" is not allowed inside update value for "${path}"`
|
|
957
|
+
);
|
|
868
958
|
}
|
|
959
|
+
assertNoNestedOperators(v, path, depth + 1);
|
|
869
960
|
}
|
|
870
|
-
return sanitizeObject(obj);
|
|
871
961
|
}
|
|
872
|
-
function
|
|
873
|
-
|
|
874
|
-
|
|
962
|
+
function sanitizeUpdate(obj, writableFields) {
|
|
963
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj))
|
|
964
|
+
throw new RestError(RestErrorCode.InvalidUpdate, "Update must be an object");
|
|
965
|
+
const input = obj;
|
|
966
|
+
const hasOperators = Object.keys(input).some((k) => k.startsWith("$"));
|
|
967
|
+
const wrapped = hasOperators ? input : { $set: input };
|
|
968
|
+
const result = {};
|
|
969
|
+
for (const [op, fields] of Object.entries(wrapped)) {
|
|
970
|
+
if (!op.startsWith("$")) {
|
|
971
|
+
throw new RestError(
|
|
972
|
+
RestErrorCode.InvalidUpdate,
|
|
973
|
+
"Update must use $-prefixed operators"
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
if (!SAFE_UPDATE_OPS.has(op)) {
|
|
977
|
+
throw new RestError(
|
|
978
|
+
RestErrorCode.InvalidUpdate,
|
|
979
|
+
`Update operator "${op}" is not allowed`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
|
|
983
|
+
throw new RestError(
|
|
984
|
+
RestErrorCode.InvalidUpdate,
|
|
985
|
+
`Operator "${op}" requires an object`
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
const validatedFields = {};
|
|
989
|
+
for (const [field, value] of Object.entries(
|
|
990
|
+
fields
|
|
991
|
+
)) {
|
|
992
|
+
if (field.startsWith("$")) {
|
|
993
|
+
throw new RestError(
|
|
994
|
+
RestErrorCode.InvalidUpdate,
|
|
995
|
+
`Nested operator "${field}" is not allowed`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
if (writableFields) {
|
|
999
|
+
if (!writableFields.has(field)) {
|
|
1000
|
+
throw new RestError(
|
|
1001
|
+
RestErrorCode.InvalidUpdate,
|
|
1002
|
+
`Field "${field}" is not writable`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (!writableFields && field.includes(".")) {
|
|
1007
|
+
throw new RestError(
|
|
1008
|
+
RestErrorCode.InvalidUpdate,
|
|
1009
|
+
`Dotted-path writes require explicit writableFields`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
assertNoNestedOperators(value, field);
|
|
1013
|
+
validatedFields[field] = value;
|
|
1014
|
+
}
|
|
1015
|
+
result[op] = validatedFields;
|
|
1016
|
+
}
|
|
1017
|
+
return result;
|
|
1018
|
+
}
|
|
1019
|
+
function projectDoc(doc, publicFields) {
|
|
1020
|
+
if (!doc || !publicFields) return doc;
|
|
1021
|
+
const out = {};
|
|
1022
|
+
for (const f of publicFields) {
|
|
1023
|
+
if (f in doc) out[f] = doc[f];
|
|
1024
|
+
}
|
|
1025
|
+
if ("_id" in doc) out._id = doc._id;
|
|
1026
|
+
return out;
|
|
1027
|
+
}
|
|
1028
|
+
function encodeCursor(key, payload) {
|
|
1029
|
+
const iv = import_node_crypto4.default.randomBytes(12);
|
|
1030
|
+
const cipher = import_node_crypto4.default.createCipheriv("aes-256-gcm", key, iv);
|
|
1031
|
+
const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
|
|
1032
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1033
|
+
const tag = cipher.getAuthTag();
|
|
1034
|
+
return [
|
|
1035
|
+
iv.toString("base64url"),
|
|
1036
|
+
tag.toString("base64url"),
|
|
1037
|
+
ciphertext.toString("base64url")
|
|
1038
|
+
].join(".");
|
|
1039
|
+
}
|
|
1040
|
+
function decodeCursor(key, token) {
|
|
1041
|
+
const parts = token.split(".");
|
|
1042
|
+
if (parts.length !== 3)
|
|
1043
|
+
throw new RestError(RestErrorCode.InvalidCursor, "Invalid cursor");
|
|
1044
|
+
const [ivB64, tagB64, ctB64] = parts;
|
|
1045
|
+
let iv, tag, ct;
|
|
1046
|
+
try {
|
|
1047
|
+
iv = Buffer.from(ivB64, "base64url");
|
|
1048
|
+
tag = Buffer.from(tagB64, "base64url");
|
|
1049
|
+
ct = Buffer.from(ctB64, "base64url");
|
|
1050
|
+
} catch {
|
|
1051
|
+
throw new RestError(RestErrorCode.InvalidCursor, "Invalid cursor encoding");
|
|
1052
|
+
}
|
|
1053
|
+
if (iv.length !== 12 || tag.length !== 16) {
|
|
1054
|
+
throw new RestError(RestErrorCode.InvalidCursor, "Invalid cursor");
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
const decipher = import_node_crypto4.default.createDecipheriv("aes-256-gcm", key, iv);
|
|
1058
|
+
decipher.setAuthTag(tag);
|
|
1059
|
+
const plaintext = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
1060
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
1061
|
+
} catch {
|
|
1062
|
+
throw new RestError(RestErrorCode.InvalidCursor, "Invalid cursor signature");
|
|
1063
|
+
}
|
|
875
1064
|
}
|
|
876
1065
|
function createRestRouter(model2, options = {}) {
|
|
877
1066
|
const {
|
|
@@ -880,8 +1069,64 @@ function createRestRouter(model2, options = {}) {
|
|
|
880
1069
|
allowDelete = true,
|
|
881
1070
|
maxLimit = 100,
|
|
882
1071
|
defaultLimit = 20,
|
|
883
|
-
defaultSort
|
|
1072
|
+
defaultSort,
|
|
1073
|
+
ownershipField,
|
|
1074
|
+
queryableFields,
|
|
1075
|
+
sortableFields,
|
|
1076
|
+
writableFields,
|
|
1077
|
+
publicFields,
|
|
1078
|
+
allowedOperators,
|
|
1079
|
+
allowRead,
|
|
1080
|
+
allowWrite
|
|
884
1081
|
} = options;
|
|
1082
|
+
const queryableSet = queryableFields ? new Set(queryableFields) : null;
|
|
1083
|
+
const sortableSet = sortableFields ? new Set(sortableFields) : null;
|
|
1084
|
+
const writableSet = writableFields ? new Set(writableFields) : null;
|
|
1085
|
+
const publicSet = publicFields ? new Set(publicFields) : null;
|
|
1086
|
+
const opsSet = allowedOperators ? new Set(allowedOperators) : DEFAULT_ALLOWED_OPS;
|
|
1087
|
+
const sanitizeFilter = buildFilterValidator(queryableSet, opsSet);
|
|
1088
|
+
const cursorKey = import_node_crypto4.default.randomBytes(32);
|
|
1089
|
+
function scopedFilter(req, filter) {
|
|
1090
|
+
if (!ownershipField) return filter;
|
|
1091
|
+
const owner = req.user?.deviceId;
|
|
1092
|
+
if (!owner)
|
|
1093
|
+
throw new RestError(
|
|
1094
|
+
RestErrorCode.AuthRequired,
|
|
1095
|
+
"Authentication required"
|
|
1096
|
+
);
|
|
1097
|
+
if (ownershipField in filter) {
|
|
1098
|
+
throw new RestError(
|
|
1099
|
+
RestErrorCode.InvalidFilter,
|
|
1100
|
+
`Field "${ownershipField}" cannot be used in filter (auto-scoped to caller)`
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
return { ...filter, [ownershipField]: owner };
|
|
1104
|
+
}
|
|
1105
|
+
function validateSort(sort) {
|
|
1106
|
+
if (!sort) return void 0;
|
|
1107
|
+
if (typeof sort !== "object" || Array.isArray(sort))
|
|
1108
|
+
throw new RestError(RestErrorCode.InvalidSort, "Sort must be an object");
|
|
1109
|
+
const out = {};
|
|
1110
|
+
for (const [field, direction] of Object.entries(sort)) {
|
|
1111
|
+
if (field.startsWith("$"))
|
|
1112
|
+
throw new RestError(
|
|
1113
|
+
RestErrorCode.InvalidSort,
|
|
1114
|
+
`Invalid sort field "${field}"`
|
|
1115
|
+
);
|
|
1116
|
+
if (sortableSet && !sortableSet.has(field))
|
|
1117
|
+
throw new RestError(
|
|
1118
|
+
RestErrorCode.InvalidSort,
|
|
1119
|
+
`Field "${field}" is not sortable`
|
|
1120
|
+
);
|
|
1121
|
+
if (direction !== 1 && direction !== -1)
|
|
1122
|
+
throw new RestError(
|
|
1123
|
+
RestErrorCode.InvalidSort,
|
|
1124
|
+
`Sort direction for "${field}" must be 1 or -1`
|
|
1125
|
+
);
|
|
1126
|
+
out[field] = direction;
|
|
1127
|
+
}
|
|
1128
|
+
return out;
|
|
1129
|
+
}
|
|
885
1130
|
const router = (0, import_express2.Router)();
|
|
886
1131
|
router.get("/", async (req, res) => {
|
|
887
1132
|
try {
|
|
@@ -889,107 +1134,172 @@ function createRestRouter(model2, options = {}) {
|
|
|
889
1134
|
const sortStr = req.query.sort;
|
|
890
1135
|
const limitStr = req.query.limit;
|
|
891
1136
|
const skipStr = req.query.skip;
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const sort =
|
|
1137
|
+
const cursorToken = req.query.cursor;
|
|
1138
|
+
const rawFilter = filterStr ? JSON.parse(filterStr) : {};
|
|
1139
|
+
let filter = sanitizeFilter(rawFilter);
|
|
1140
|
+
filter = scopedFilter(req, filter);
|
|
1141
|
+
const rawSort = sortStr ? JSON.parse(sortStr) : defaultSort;
|
|
1142
|
+
const sort = validateSort(rawSort);
|
|
898
1143
|
const limit = Math.min(Number(limitStr) || defaultLimit, maxLimit);
|
|
899
1144
|
const skip = Number(skipStr) || 0;
|
|
900
1145
|
let query = model2.find(filter);
|
|
901
1146
|
if (sort) query = query.sort(sort);
|
|
902
|
-
if (
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
query = query.startAfter({ [sortField]: cursorValue });
|
|
1147
|
+
if (cursorToken && sort) {
|
|
1148
|
+
const decoded = decodeCursor(cursorKey, cursorToken);
|
|
1149
|
+
query = query.startAfter(decoded);
|
|
906
1150
|
}
|
|
907
1151
|
if (skip) query = query.skip(skip);
|
|
908
1152
|
query = query.limit(limit);
|
|
909
1153
|
const docs = await query.exec();
|
|
910
1154
|
let nextCursor = null;
|
|
911
1155
|
if (docs.length === limit && sort) {
|
|
912
|
-
const
|
|
913
|
-
|
|
1156
|
+
const last = docs[docs.length - 1];
|
|
1157
|
+
const cursorPayload = {};
|
|
1158
|
+
for (const field of Object.keys(sort)) {
|
|
1159
|
+
cursorPayload[field] = last[field];
|
|
1160
|
+
}
|
|
1161
|
+
nextCursor = encodeCursor(cursorKey, cursorPayload);
|
|
914
1162
|
}
|
|
915
|
-
|
|
1163
|
+
const projected = publicSet ? docs.map((d) => projectDoc(d, publicSet)) : docs;
|
|
1164
|
+
res.json({ success: true, data: projected, nextCursor });
|
|
916
1165
|
} catch (error) {
|
|
917
|
-
res
|
|
1166
|
+
handleCaught(res, error);
|
|
918
1167
|
}
|
|
919
1168
|
});
|
|
920
1169
|
router.get("/:id", async (req, res) => {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1170
|
+
try {
|
|
1171
|
+
const id = param(req, "id");
|
|
1172
|
+
const doc = await model2.findById(id);
|
|
1173
|
+
if (!doc) {
|
|
1174
|
+
fail(res, 404, RestErrorCode.NotFound, "Not found");
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (ownershipField) {
|
|
1178
|
+
const owner = req.user?.deviceId;
|
|
1179
|
+
if (!owner || doc[ownershipField] !== owner) {
|
|
1180
|
+
fail(res, 404, RestErrorCode.NotFound, "Not found");
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (allowRead && !await allowRead(req, doc)) {
|
|
1185
|
+
fail(res, 403, RestErrorCode.ReadForbidden, "Read not allowed");
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
res.json({ success: true, data: projectDoc(doc, publicSet) });
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
handleCaught(res, error);
|
|
926
1191
|
}
|
|
927
|
-
res.json({ success: true, data: doc });
|
|
928
1192
|
});
|
|
929
1193
|
router.post("/", async (req, res) => {
|
|
930
1194
|
if (!allowCreate) {
|
|
931
|
-
res
|
|
1195
|
+
fail(res, 403, RestErrorCode.CreateDisabled, "Create not allowed");
|
|
932
1196
|
return;
|
|
933
1197
|
}
|
|
934
1198
|
try {
|
|
935
|
-
const data = req.body;
|
|
1199
|
+
const data = { ...req.body };
|
|
1200
|
+
if (ownershipField) {
|
|
1201
|
+
const owner = req.user?.deviceId;
|
|
1202
|
+
if (!owner) {
|
|
1203
|
+
fail(res, 401, RestErrorCode.AuthRequired, "Authentication required");
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
data[ownershipField] = owner;
|
|
1207
|
+
}
|
|
936
1208
|
if (!data._id) data._id = import_node_crypto4.default.randomUUID();
|
|
937
1209
|
const doc = await model2.create(data);
|
|
938
|
-
res.status(201).json({ success: true, data: doc });
|
|
1210
|
+
res.status(201).json({ success: true, data: projectDoc(doc, publicSet) });
|
|
939
1211
|
} catch (error) {
|
|
940
|
-
res
|
|
1212
|
+
handleCaught(res, error);
|
|
941
1213
|
}
|
|
942
1214
|
});
|
|
943
1215
|
router.post("/count", async (req, res) => {
|
|
944
1216
|
try {
|
|
945
|
-
const
|
|
1217
|
+
const rawFilter = req.body.filter ?? {};
|
|
1218
|
+
let filter = sanitizeFilter(rawFilter);
|
|
1219
|
+
filter = scopedFilter(req, filter);
|
|
946
1220
|
const count = await model2.count(filter);
|
|
947
1221
|
res.json({ success: true, count });
|
|
948
1222
|
} catch (error) {
|
|
949
|
-
res
|
|
1223
|
+
handleCaught(res, error);
|
|
950
1224
|
}
|
|
951
1225
|
});
|
|
952
1226
|
router.patch("/:id", async (req, res) => {
|
|
953
1227
|
if (!allowUpdate) {
|
|
954
|
-
res
|
|
1228
|
+
fail(res, 403, RestErrorCode.UpdateDisabled, "Update not allowed");
|
|
955
1229
|
return;
|
|
956
1230
|
}
|
|
957
1231
|
const id = param(req, "id");
|
|
958
1232
|
try {
|
|
959
|
-
const update = sanitizeUpdate(req.body.update ?? req.body);
|
|
1233
|
+
const update = sanitizeUpdate(req.body.update ?? req.body, writableSet);
|
|
1234
|
+
if (ownershipField || allowWrite) {
|
|
1235
|
+
const existing = await model2.findById(id);
|
|
1236
|
+
if (!existing) {
|
|
1237
|
+
fail(res, 404, RestErrorCode.NotFound, "Not found");
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (ownershipField) {
|
|
1241
|
+
const owner = req.user?.deviceId;
|
|
1242
|
+
if (!owner || existing[ownershipField] !== owner) {
|
|
1243
|
+
fail(res, 404, RestErrorCode.NotFound, "Not found");
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (allowWrite && !await allowWrite(req, existing, update)) {
|
|
1248
|
+
fail(res, 403, RestErrorCode.WriteForbidden, "Write not allowed");
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
960
1252
|
const success = await model2.updateById(id, update);
|
|
961
1253
|
if (!success) {
|
|
962
|
-
res
|
|
1254
|
+
fail(res, 404, RestErrorCode.NotFound, "Not found");
|
|
963
1255
|
return;
|
|
964
1256
|
}
|
|
965
1257
|
const doc = await model2.findById(id);
|
|
966
|
-
res.json({ success: true, data: doc });
|
|
1258
|
+
res.json({ success: true, data: projectDoc(doc, publicSet) });
|
|
967
1259
|
} catch (error) {
|
|
968
|
-
res
|
|
1260
|
+
handleCaught(res, error);
|
|
969
1261
|
}
|
|
970
1262
|
});
|
|
971
1263
|
router.delete("/:id", async (req, res) => {
|
|
1264
|
+
if (allowDelete === false) {
|
|
1265
|
+
fail(res, 403, RestErrorCode.DeleteDisabled, "Delete not allowed");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
972
1268
|
const id = param(req, "id");
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
if (
|
|
976
|
-
|
|
977
|
-
|
|
1269
|
+
try {
|
|
1270
|
+
const needsDoc = !!ownershipField || typeof allowDelete === "function";
|
|
1271
|
+
if (needsDoc) {
|
|
1272
|
+
const doc = await model2.findById(id);
|
|
1273
|
+
if (!doc) {
|
|
1274
|
+
fail(res, 404, RestErrorCode.NotFound, "Not found");
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (ownershipField) {
|
|
1278
|
+
const owner = req.user?.deviceId;
|
|
1279
|
+
if (!owner || doc[ownershipField] !== owner) {
|
|
1280
|
+
fail(res, 404, RestErrorCode.NotFound, "Not found");
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (typeof allowDelete === "function" && !await allowDelete(req, doc)) {
|
|
1285
|
+
fail(
|
|
1286
|
+
res,
|
|
1287
|
+
403,
|
|
1288
|
+
RestErrorCode.DeleteForbidden,
|
|
1289
|
+
"Delete not allowed"
|
|
1290
|
+
);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
978
1293
|
}
|
|
979
|
-
|
|
980
|
-
|
|
1294
|
+
const success = await model2.deleteById(id);
|
|
1295
|
+
if (!success) {
|
|
1296
|
+
fail(res, 404, RestErrorCode.NotFound, "Not found");
|
|
981
1297
|
return;
|
|
982
1298
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
}
|
|
987
|
-
const success = await model2.deleteById(id);
|
|
988
|
-
if (!success) {
|
|
989
|
-
res.status(404).json({ success: false, error: "Not found" });
|
|
990
|
-
return;
|
|
1299
|
+
res.json({ success: true });
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
handleCaught(res, error);
|
|
991
1302
|
}
|
|
992
|
-
res.json({ success: true });
|
|
993
1303
|
});
|
|
994
1304
|
return router;
|
|
995
1305
|
}
|
|
@@ -1019,7 +1329,7 @@ var CodixusServer = class {
|
|
|
1019
1329
|
async connect() {
|
|
1020
1330
|
this._db = await this.connection.connect();
|
|
1021
1331
|
this.banService = new BanService(this._db);
|
|
1022
|
-
this.guardFn = createGuard(this.jwt);
|
|
1332
|
+
this.guardFn = createGuard(this.jwt, this.banService);
|
|
1023
1333
|
for (const m of getModelRegistry()) {
|
|
1024
1334
|
m._bind(this._db, this.instanceId);
|
|
1025
1335
|
await m._ensureIndexes();
|
|
@@ -1111,6 +1421,7 @@ var import_shared4 = require("@codixus/shared");
|
|
|
1111
1421
|
ErrorCodes,
|
|
1112
1422
|
Model,
|
|
1113
1423
|
Query,
|
|
1424
|
+
RestErrorCode,
|
|
1114
1425
|
SubCollection,
|
|
1115
1426
|
model,
|
|
1116
1427
|
validate
|