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