@airoom/nextmin-node 0.1.4 → 0.1.6
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/api/apiRouter.d.ts +6 -20
- package/dist/api/apiRouter.js +86 -1476
- package/dist/api/router/ctx.d.ts +25 -0
- package/dist/api/router/ctx.js +2 -0
- package/dist/api/router/mountCrudRoutes.d.ts +2 -0
- package/dist/api/router/mountCrudRoutes.js +754 -0
- package/dist/api/router/mountFindRoutes.d.ts +2 -0
- package/dist/api/router/mountFindRoutes.js +205 -0
- package/dist/api/router/setupAuthRoutes.d.ts +2 -0
- package/dist/api/router/setupAuthRoutes.js +247 -0
- package/dist/api/router/setupFileRoutes.d.ts +2 -0
- package/dist/api/router/setupFileRoutes.js +85 -0
- package/dist/api/router/utils.d.ts +63 -0
- package/dist/api/router/utils.js +247 -0
- package/dist/database/MongoAdapter.d.ts +1 -1
- package/dist/database/MongoAdapter.js +21 -32
- package/dist/schemas/Roles.json +7 -2
- package/dist/utils/DefaultDataInitializer.js +3 -0
- package/dist/utils/SchemaLoader.js +28 -7
- package/package.json +1 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mountFindRoutes = mountFindRoutes;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
const authorize_1 = require("../../policy/authorize");
|
|
6
|
+
function mountFindRoutes(ctx) {
|
|
7
|
+
const { router } = ctx;
|
|
8
|
+
const ctxFromReq = (req) => {
|
|
9
|
+
const raw = req.user?.role;
|
|
10
|
+
let roleStr = null;
|
|
11
|
+
if (typeof raw === "string")
|
|
12
|
+
roleStr = raw;
|
|
13
|
+
else if (Array.isArray(raw)) {
|
|
14
|
+
const first = raw[0];
|
|
15
|
+
if (typeof first === "string")
|
|
16
|
+
roleStr = first;
|
|
17
|
+
else if (first && typeof first === "object" && typeof first.name === "string") {
|
|
18
|
+
roleStr = first.name;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (raw && typeof raw === "object" && typeof raw.name === "string") {
|
|
22
|
+
roleStr = raw.name;
|
|
23
|
+
}
|
|
24
|
+
roleStr = roleStr ? roleStr.toLowerCase() : null;
|
|
25
|
+
return {
|
|
26
|
+
isAuthenticated: !!req.user,
|
|
27
|
+
role: roleStr,
|
|
28
|
+
userId: req.user?.id ?? req.user?._id ?? null,
|
|
29
|
+
isSuperadmin: roleStr === "superadmin",
|
|
30
|
+
apiKeyOk: true,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const safeGetContainerRefIDs = async (containerModelLC, id, refField) => {
|
|
34
|
+
const containerSchema = ctx.getSchema(containerModelLC);
|
|
35
|
+
const adapterAny = ctx.dbAdapter;
|
|
36
|
+
if (typeof adapterAny.findOne === "function") {
|
|
37
|
+
try {
|
|
38
|
+
const raw = await adapterAny.findOne(containerSchema.modelName, { id }, { projection: { [refField]: 1 } });
|
|
39
|
+
return (0, utils_1.extractIds)(raw?.[refField]);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
const containerDoc = (await ctx.getModel(containerModelLC).read({ id }, 1, 0, true))?.[0];
|
|
44
|
+
return (0, utils_1.extractIds)(containerDoc?.[refField]);
|
|
45
|
+
};
|
|
46
|
+
// FORWARD
|
|
47
|
+
router.get("/find/:container/:refField/:id", ctx.optionalAuthMiddleware, async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const containerLC = String(req.params.container || "").toLowerCase();
|
|
50
|
+
const refField = String(req.params.refField || "");
|
|
51
|
+
const id = String(req.params.id || "");
|
|
52
|
+
const schema = ctx.getSchema(containerLC);
|
|
53
|
+
const attr = schema.attributes?.[refField];
|
|
54
|
+
const rinfo = (0, utils_1.refInfoFromAttr)(attr);
|
|
55
|
+
if (!rinfo) {
|
|
56
|
+
return res.status(400).json({ error: true, message: "refField is not a reference field" });
|
|
57
|
+
}
|
|
58
|
+
const { limit, page, skip, projection, sort } = (0, utils_1.parseQuery)(req);
|
|
59
|
+
const cPolicy = { allowedMethods: schema.allowedMethods, access: schema.access };
|
|
60
|
+
const cDec = (0, authorize_1.authorize)(containerLC, "read", cPolicy, ctxFromReq(req));
|
|
61
|
+
if (!cDec.allow)
|
|
62
|
+
return res.status(403).json({ error: true, message: "forbidden" });
|
|
63
|
+
const ids = await safeGetContainerRefIDs(containerLC, id, refField);
|
|
64
|
+
const targetLC = String(rinfo.ref || "").toLowerCase();
|
|
65
|
+
const targetSchema = ctx.getSchema(targetLC);
|
|
66
|
+
const tPolicy = { allowedMethods: targetSchema.allowedMethods, access: targetSchema.access };
|
|
67
|
+
const tDec = (0, authorize_1.authorize)(targetLC, "read", tPolicy, ctxFromReq(req));
|
|
68
|
+
if (!tDec.allow)
|
|
69
|
+
return res.status(403).json({ error: true, message: "forbidden" });
|
|
70
|
+
if (ids.length === 0) {
|
|
71
|
+
return res.status(200).json({
|
|
72
|
+
success: true,
|
|
73
|
+
message: `Data fetched for ${targetSchema.modelName}`,
|
|
74
|
+
data: [],
|
|
75
|
+
pagination: { totalRows: 0, page, limit },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const adapterAny = ctx.dbAdapter;
|
|
79
|
+
let items = [];
|
|
80
|
+
if (typeof adapterAny.findMany === "function") {
|
|
81
|
+
items = await adapterAny.findMany(targetSchema.modelName, { id: { $in: ids } }, { sort, skip, limit, projection });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
items = await ctx.getModel(targetLC).read({ id: { $in: ids } }, limit, skip, !!tDec.exposePrivate);
|
|
85
|
+
}
|
|
86
|
+
// extended hydration if needed
|
|
87
|
+
if (targetSchema.extends && Array.isArray(items) && items.length) {
|
|
88
|
+
const baseName = String(targetSchema.extends);
|
|
89
|
+
const baseLC = baseName.toLowerCase();
|
|
90
|
+
const baseModel = ctx.getModel(baseLC);
|
|
91
|
+
const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
|
|
92
|
+
if (baseIds.length) {
|
|
93
|
+
const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
|
|
94
|
+
const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
|
|
95
|
+
items = items.map((it) => {
|
|
96
|
+
const bid = String(it?.baseId || "");
|
|
97
|
+
const b = bid ? baseMap.get(bid) : null;
|
|
98
|
+
const merged = b ? { ...b, ...it } : { ...it };
|
|
99
|
+
delete merged.baseId;
|
|
100
|
+
return merged;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
items = items.map((it) => {
|
|
105
|
+
const copy = { ...it };
|
|
106
|
+
delete copy.baseId;
|
|
107
|
+
return copy;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const masked = tDec.exposePrivate ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask) : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
|
|
112
|
+
const totalRows = ids.length;
|
|
113
|
+
return res.status(200).json({
|
|
114
|
+
success: true,
|
|
115
|
+
message: `Data fetched for ${targetSchema.modelName}`,
|
|
116
|
+
data: masked,
|
|
117
|
+
pagination: { totalRows, page, limit },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (err?.code === "MODEL_REMOVED") {
|
|
122
|
+
return res.status(410).json({ error: true, message: err.message });
|
|
123
|
+
}
|
|
124
|
+
return res.status(400).json({ error: true, message: err?.message || "Error" });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// REVERSE
|
|
128
|
+
router.get("/find/reverse/:target/:byField/:id", ctx.optionalAuthMiddleware, async (req, res) => {
|
|
129
|
+
try {
|
|
130
|
+
const targetLC = String(req.params.target || "").toLowerCase();
|
|
131
|
+
const byField = String(req.params.byField || "");
|
|
132
|
+
const id = String(req.params.id || "");
|
|
133
|
+
const targetSchema = ctx.getSchema(targetLC);
|
|
134
|
+
const attr = targetSchema.attributes?.[byField];
|
|
135
|
+
const rinfo = (0, utils_1.refInfoFromAttr)(attr);
|
|
136
|
+
if (!rinfo) {
|
|
137
|
+
return res.status(400).json({
|
|
138
|
+
error: true,
|
|
139
|
+
message: "byField is not a reference field",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const { limit, page, skip, projection, sort } = (0, utils_1.parseQuery)(req);
|
|
143
|
+
const tPolicy = { allowedMethods: targetSchema.allowedMethods, access: targetSchema.access };
|
|
144
|
+
const tDec = (0, authorize_1.authorize)(targetLC, "read", tPolicy, ctxFromReq(req));
|
|
145
|
+
if (!tDec.allow)
|
|
146
|
+
return res.status(403).json({ error: true, message: "forbidden" });
|
|
147
|
+
const filter = rinfo.isArray ? { [byField]: { $in: [id] } } : { [byField]: id };
|
|
148
|
+
const adapterAny = ctx.dbAdapter;
|
|
149
|
+
let items = [];
|
|
150
|
+
let totalRows = 0;
|
|
151
|
+
if (typeof adapterAny.findMany === "function") {
|
|
152
|
+
items = await adapterAny.findMany(targetSchema.modelName, filter, { sort, skip, limit, projection });
|
|
153
|
+
if (typeof adapterAny.count === "function") {
|
|
154
|
+
totalRows = await adapterAny.count(targetSchema.modelName, filter);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const all = await ctx.getModel(targetLC).read(filter, 0, 0, !!tDec.exposePrivate);
|
|
158
|
+
totalRows = all.length;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const all = await ctx.getModel(targetLC).read(filter, 0, 0, !!tDec.exposePrivate);
|
|
163
|
+
totalRows = all.length;
|
|
164
|
+
items = all.slice(skip, skip + limit);
|
|
165
|
+
}
|
|
166
|
+
if (targetSchema.extends && Array.isArray(items) && items.length) {
|
|
167
|
+
const baseName = String(targetSchema.extends);
|
|
168
|
+
const baseLC = baseName.toLowerCase();
|
|
169
|
+
const baseModel = ctx.getModel(baseLC);
|
|
170
|
+
const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
|
|
171
|
+
if (baseIds.length) {
|
|
172
|
+
const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
|
|
173
|
+
const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
|
|
174
|
+
items = items.map((it) => {
|
|
175
|
+
const bid = String(it?.baseId || "");
|
|
176
|
+
const b = bid ? baseMap.get(bid) : null;
|
|
177
|
+
const merged = b ? { ...b, ...it } : { ...it };
|
|
178
|
+
delete merged.baseId;
|
|
179
|
+
return merged;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
items = items.map((it) => {
|
|
184
|
+
const copy = { ...it };
|
|
185
|
+
delete copy.baseId;
|
|
186
|
+
return copy;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const masked = tDec.exposePrivate ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask) : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
|
|
191
|
+
return res.status(200).json({
|
|
192
|
+
success: true,
|
|
193
|
+
message: `Data fetched for ${targetSchema.modelName}`,
|
|
194
|
+
data: masked,
|
|
195
|
+
pagination: { totalRows, page, limit },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
if (err?.code === "MODEL_REMOVED") {
|
|
200
|
+
return res.status(410).json({ error: true, message: err.message });
|
|
201
|
+
}
|
|
202
|
+
return res.status(400).json({ error: true, message: err?.message || "Error" });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setupAuthRoutes = setupAuthRoutes;
|
|
7
|
+
const bcrypt_1 = __importDefault(require("bcrypt"));
|
|
8
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
|
+
function setupAuthRoutes(ctx) {
|
|
10
|
+
const basePath = '/auth/users';
|
|
11
|
+
// REGISTER
|
|
12
|
+
ctx.router.post(`${basePath}/register`, ctx.apiKeyMiddleware, async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
let { email, username, password, ...rest } = req.body;
|
|
15
|
+
if (String(password).length < 8) {
|
|
16
|
+
return res
|
|
17
|
+
.status(400)
|
|
18
|
+
.json({
|
|
19
|
+
error: true,
|
|
20
|
+
message: 'Password must be at least 8 characters long',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const userModel = ctx.getModel('users');
|
|
24
|
+
const usersSchema = ctx.getSchema('users');
|
|
25
|
+
if (email)
|
|
26
|
+
email = String(email).trim().toLowerCase();
|
|
27
|
+
if (username)
|
|
28
|
+
username = String(username).trim();
|
|
29
|
+
const data = { ...rest };
|
|
30
|
+
if (email)
|
|
31
|
+
data.email = email;
|
|
32
|
+
if (username)
|
|
33
|
+
data.username = username;
|
|
34
|
+
if (data.status == null)
|
|
35
|
+
data.status = 'pending';
|
|
36
|
+
if (usersSchema) {
|
|
37
|
+
const missing = ctx.validateRequiredFields(usersSchema, {
|
|
38
|
+
...data,
|
|
39
|
+
password,
|
|
40
|
+
});
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
return res.status(400).json({
|
|
43
|
+
error: true,
|
|
44
|
+
message: `Missing required field(s): ${missing.join(', ')}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const conflicts = await ctx.checkUniqueFields(usersSchema, data);
|
|
48
|
+
if (conflicts && conflicts.length) {
|
|
49
|
+
return res.status(400).json({
|
|
50
|
+
error: true,
|
|
51
|
+
message: `Duplicate value for field(s): ${conflicts.join(', ')}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const salt = await bcrypt_1.default.genSalt(10);
|
|
56
|
+
const hashed = await bcrypt_1.default.hash(String(password) + ctx.jwtSecret, salt);
|
|
57
|
+
data.password = hashed;
|
|
58
|
+
const created = await userModel.create(data);
|
|
59
|
+
const roleName = (await ctx.normalizeRoleName(created.role)) ?? '';
|
|
60
|
+
const token = jsonwebtoken_1.default.sign({ id: created.id, role: roleName }, ctx.jwtSecret, { expiresIn: '7days' });
|
|
61
|
+
delete created.password;
|
|
62
|
+
return res.json({
|
|
63
|
+
success: true,
|
|
64
|
+
message: 'Registration successful.',
|
|
65
|
+
data: { token, user: created },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
ctx.handleWriteError(error, res);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// LOGIN
|
|
73
|
+
ctx.router.post(`${basePath}/login`, ctx.apiKeyMiddleware, async (req, res) => {
|
|
74
|
+
let { email, username, password } = req.body;
|
|
75
|
+
if ((!email && !username) || !password) {
|
|
76
|
+
return res
|
|
77
|
+
.status(400)
|
|
78
|
+
.json({
|
|
79
|
+
error: true,
|
|
80
|
+
message: 'Email/username and password are required',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const userModel = ctx.getModel('users');
|
|
85
|
+
const findBy = email
|
|
86
|
+
? { email: String(email).trim().toLowerCase() }
|
|
87
|
+
: { username: String(username).trim() };
|
|
88
|
+
const users = await userModel.read(findBy, 1, 0, true);
|
|
89
|
+
const user = users?.[0];
|
|
90
|
+
if (!user)
|
|
91
|
+
return res
|
|
92
|
+
.status(400)
|
|
93
|
+
.json({ error: true, message: 'Invalid credentials' });
|
|
94
|
+
const hashedPassword = user.password;
|
|
95
|
+
if (!hashedPassword)
|
|
96
|
+
return res
|
|
97
|
+
.status(400)
|
|
98
|
+
.json({ error: true, message: 'User password not set' });
|
|
99
|
+
const isMatch = await bcrypt_1.default.compare(String(password) + ctx.jwtSecret, hashedPassword);
|
|
100
|
+
if (!isMatch)
|
|
101
|
+
return res
|
|
102
|
+
.status(400)
|
|
103
|
+
.json({ error: true, message: 'Invalid credentials' });
|
|
104
|
+
const status = user.status;
|
|
105
|
+
if (status && status !== 'active') {
|
|
106
|
+
const msg = status === 'pending'
|
|
107
|
+
? 'Your account is awaiting approval.'
|
|
108
|
+
: 'Your account is suspended.';
|
|
109
|
+
return res.status(403).json({ error: true, message: msg });
|
|
110
|
+
}
|
|
111
|
+
const roleName = (await ctx.normalizeRoleName(user.role)) ?? '';
|
|
112
|
+
const token = jsonwebtoken_1.default.sign({ id: user.id, role: roleName }, ctx.jwtSecret, { expiresIn: '7days' });
|
|
113
|
+
delete user.password;
|
|
114
|
+
res.json({
|
|
115
|
+
success: true,
|
|
116
|
+
message: 'You are successfully logged in.',
|
|
117
|
+
data: { token, user },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error?.code === 'MODEL_REMOVED')
|
|
122
|
+
return res.status(410).json({ error: true, message: error.message });
|
|
123
|
+
res.status(500).json({ error: true, message: error.message });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// ME
|
|
127
|
+
ctx.router.get(`${basePath}/me`, ctx.apiKeyMiddleware, ctx.authenticateMiddleware, async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const userId = req.user?.id ?? req.user?._id ?? null;
|
|
130
|
+
if (!userId) {
|
|
131
|
+
return res
|
|
132
|
+
.status(401)
|
|
133
|
+
.json({ error: true, message: 'Not authenticated' });
|
|
134
|
+
}
|
|
135
|
+
const userModel = ctx.getModel('users');
|
|
136
|
+
const arr = await userModel.read({ id: userId }, 1, 0, true);
|
|
137
|
+
const user = arr?.[0];
|
|
138
|
+
if (!user) {
|
|
139
|
+
return res
|
|
140
|
+
.status(404)
|
|
141
|
+
.json({ error: true, message: 'User not found' });
|
|
142
|
+
}
|
|
143
|
+
delete user.password;
|
|
144
|
+
return res.json({ success: true, data: user });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return res
|
|
148
|
+
.status(400)
|
|
149
|
+
.json({ error: true, message: error?.message || 'Error' });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// CHANGE PASSWORD
|
|
153
|
+
ctx.router.post(`${basePath}/change-password`, ctx.apiKeyMiddleware, ctx.authenticateMiddleware, async (req, res) => {
|
|
154
|
+
try {
|
|
155
|
+
const { oldPassword, newPassword } = req.body;
|
|
156
|
+
if (!oldPassword || !newPassword) {
|
|
157
|
+
return res
|
|
158
|
+
.status(400)
|
|
159
|
+
.json({
|
|
160
|
+
error: true,
|
|
161
|
+
message: 'oldPassword and newPassword are required',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (String(newPassword).length < 8) {
|
|
165
|
+
return res
|
|
166
|
+
.status(400)
|
|
167
|
+
.json({
|
|
168
|
+
error: true,
|
|
169
|
+
message: 'New password must be at least 8 characters long',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (String(newPassword) === String(oldPassword)) {
|
|
173
|
+
return res
|
|
174
|
+
.status(400)
|
|
175
|
+
.json({
|
|
176
|
+
error: true,
|
|
177
|
+
message: 'New password must be different from old password',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const userId = req.user?.id ?? req.user?._id ?? null;
|
|
181
|
+
if (!userId) {
|
|
182
|
+
return res
|
|
183
|
+
.status(401)
|
|
184
|
+
.json({ error: true, message: 'Not authenticated' });
|
|
185
|
+
}
|
|
186
|
+
const userModel = ctx.getModel('users');
|
|
187
|
+
const arr = await userModel.read({ id: userId }, 1, 0, true);
|
|
188
|
+
const user = arr?.[0];
|
|
189
|
+
if (!user) {
|
|
190
|
+
return res
|
|
191
|
+
.status(404)
|
|
192
|
+
.json({ error: true, message: 'User not found' });
|
|
193
|
+
}
|
|
194
|
+
const storedHash = user.password;
|
|
195
|
+
if (!storedHash) {
|
|
196
|
+
return res
|
|
197
|
+
.status(400)
|
|
198
|
+
.json({ error: true, message: 'User password not set' });
|
|
199
|
+
}
|
|
200
|
+
const match = await bcrypt_1.default.compare(String(oldPassword) + ctx.jwtSecret, storedHash);
|
|
201
|
+
if (!match) {
|
|
202
|
+
return res
|
|
203
|
+
.status(400)
|
|
204
|
+
.json({ error: true, message: 'Old password is incorrect' });
|
|
205
|
+
}
|
|
206
|
+
const salt = await bcrypt_1.default.genSalt(10);
|
|
207
|
+
const newHash = await bcrypt_1.default.hash(String(newPassword) + ctx.jwtSecret, salt);
|
|
208
|
+
await userModel.update(user.id, { password: newHash });
|
|
209
|
+
return res.json({
|
|
210
|
+
success: true,
|
|
211
|
+
message: 'Password changed successfully.',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
res
|
|
216
|
+
.status(400)
|
|
217
|
+
.json({ error: true, message: error?.message || 'Error' });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
// FORGOT PASSWORD
|
|
221
|
+
const forgotPasswordHandler = async (req, res) => {
|
|
222
|
+
try {
|
|
223
|
+
const { email } = (req.body || {});
|
|
224
|
+
if (!email || !String(email).trim()) {
|
|
225
|
+
return res
|
|
226
|
+
.status(400)
|
|
227
|
+
.json({ error: true, message: 'Email is required' });
|
|
228
|
+
}
|
|
229
|
+
const normalizedEmail = String(email).trim().toLowerCase();
|
|
230
|
+
try {
|
|
231
|
+
const userModel = ctx.getModel('users');
|
|
232
|
+
await userModel.read({ email: normalizedEmail }, 1, 0, true);
|
|
233
|
+
}
|
|
234
|
+
catch { }
|
|
235
|
+
return res.json({
|
|
236
|
+
success: true,
|
|
237
|
+
message: 'If an account exists, reset instructions have been sent.',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
return res
|
|
242
|
+
.status(400)
|
|
243
|
+
.json({ error: true, message: error?.message || 'Error' });
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
ctx.router.post(`${basePath}/forgot-password`, ctx.apiKeyMiddleware, forgotPasswordHandler);
|
|
247
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setupFileRoutes = setupFileRoutes;
|
|
7
|
+
const multer_1 = __importDefault(require("multer"));
|
|
8
|
+
const filename_1 = require("../../files/filename");
|
|
9
|
+
function setupFileRoutes(ctx) {
|
|
10
|
+
if (!ctx.fileStorage)
|
|
11
|
+
return;
|
|
12
|
+
const upload = (0, multer_1.default)({
|
|
13
|
+
storage: multer_1.default.memoryStorage(),
|
|
14
|
+
limits: { fileSize: 50 * 1024 * 1024, files: 20 },
|
|
15
|
+
});
|
|
16
|
+
const fileAuthMiddleware = (req, res, next) => ctx.authenticateMiddleware(req, res, next);
|
|
17
|
+
ctx.router.post("/files", fileAuthMiddleware, upload.any(), async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const files = req.files ?? [];
|
|
20
|
+
if (!files.length) {
|
|
21
|
+
return res.status(400).json({ error: true, message: "No files uploaded" });
|
|
22
|
+
}
|
|
23
|
+
const results = await Promise.all(files.map(async (f) => {
|
|
24
|
+
const folder = shortFolder();
|
|
25
|
+
const ext = (f.originalname.match(/\.([A-Za-z0-9]{1,8})$/)?.[1] ??
|
|
26
|
+
(0, filename_1.extFromMime)(f.mimetype) ??
|
|
27
|
+
"bin").toLowerCase();
|
|
28
|
+
const key = `${folder}/${shortUid()}.${ext}`;
|
|
29
|
+
const out = await ctx.fileStorage.upload({
|
|
30
|
+
key,
|
|
31
|
+
body: f.buffer,
|
|
32
|
+
contentType: f.mimetype,
|
|
33
|
+
metadata: { originalName: f.originalname || "" },
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
provider: out.provider,
|
|
37
|
+
bucket: out.bucket,
|
|
38
|
+
key: out.key,
|
|
39
|
+
url: out.url,
|
|
40
|
+
etag: out.etag,
|
|
41
|
+
contentType: out.contentType,
|
|
42
|
+
size: out.size,
|
|
43
|
+
metadata: out.metadata,
|
|
44
|
+
originalName: f.originalname,
|
|
45
|
+
};
|
|
46
|
+
}));
|
|
47
|
+
return res.json({
|
|
48
|
+
success: true,
|
|
49
|
+
message: "Files uploaded successfully",
|
|
50
|
+
data: results,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return res.status(400).json({ error: true, message: err?.message ?? "Upload failed" });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
ctx.router.delete("/files/:key(*)", fileAuthMiddleware, async (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const key = String(req.params.key || "");
|
|
60
|
+
if (!key) {
|
|
61
|
+
return res.status(400).json({ error: true, message: "Key is required" });
|
|
62
|
+
}
|
|
63
|
+
const { deleted } = await ctx.fileStorage.delete(key);
|
|
64
|
+
return res.json({
|
|
65
|
+
success: true,
|
|
66
|
+
message: deleted ? "File deleted" : "Delete attempted",
|
|
67
|
+
key,
|
|
68
|
+
deleted,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return res.status(400).json({ error: true, message: err?.message ?? "Delete failed" });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function shortFolder() {
|
|
77
|
+
const d = new Date();
|
|
78
|
+
const y = d.getFullYear();
|
|
79
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
80
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
81
|
+
return `uploads/${y}/${m}/${day}`;
|
|
82
|
+
}
|
|
83
|
+
function shortUid() {
|
|
84
|
+
return (Date.now().toString(36) + Math.random().toString(36).slice(2, 6)).toLowerCase();
|
|
85
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Request } from "express";
|
|
2
|
+
export type AnyRec = Record<string, any>;
|
|
3
|
+
export type SortDir = 1 | -1;
|
|
4
|
+
export type SortSpec = Record<string, SortDir>;
|
|
5
|
+
export declare const isPlainObject: (v: unknown) => v is AnyRec;
|
|
6
|
+
export declare const splitCSV: (raw: string) => string[];
|
|
7
|
+
export declare function normalizeAttrType(attr: any): string;
|
|
8
|
+
export declare function toIdString(v: any): string | null;
|
|
9
|
+
export declare function splitFilterForExtended(filter: AnyRec, baseKeys: Set<string>): {
|
|
10
|
+
child: AnyRec;
|
|
11
|
+
base: AnyRec;
|
|
12
|
+
};
|
|
13
|
+
export declare function splitSortForExtended(sort: SortSpec, baseKeys: Set<string>): {
|
|
14
|
+
child: SortSpec;
|
|
15
|
+
base: SortSpec;
|
|
16
|
+
};
|
|
17
|
+
export declare function sortInMemory(rows: AnyRec[], sort: SortSpec): AnyRec[];
|
|
18
|
+
export declare function matchDoc(doc: AnyRec, filter: AnyRec): boolean;
|
|
19
|
+
export declare function buildPredicateForField(field: string, attr: any, raw: string): {
|
|
20
|
+
[field]: {
|
|
21
|
+
$in: string[];
|
|
22
|
+
};
|
|
23
|
+
} | {
|
|
24
|
+
[field]: {
|
|
25
|
+
$regex: string;
|
|
26
|
+
$options: string;
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
[field]: {
|
|
30
|
+
$in: number[];
|
|
31
|
+
};
|
|
32
|
+
} | {
|
|
33
|
+
[field]: number;
|
|
34
|
+
} | {
|
|
35
|
+
[field]: {
|
|
36
|
+
$in: boolean[];
|
|
37
|
+
};
|
|
38
|
+
} | {
|
|
39
|
+
[field]: boolean;
|
|
40
|
+
} | {
|
|
41
|
+
[field]: string;
|
|
42
|
+
} | {
|
|
43
|
+
[field]: {
|
|
44
|
+
$in: Date[];
|
|
45
|
+
};
|
|
46
|
+
} | {
|
|
47
|
+
[field]: Date;
|
|
48
|
+
} | null;
|
|
49
|
+
export declare function parseSort(expr?: string): Record<string, 1 | -1> | undefined;
|
|
50
|
+
export declare function parseQuery(req: Request): {
|
|
51
|
+
limit: number;
|
|
52
|
+
page: number;
|
|
53
|
+
skip: number;
|
|
54
|
+
projection: {
|
|
55
|
+
[k: string]: 1;
|
|
56
|
+
} | undefined;
|
|
57
|
+
sort: Record<string, 1 | -1> | undefined;
|
|
58
|
+
};
|
|
59
|
+
export declare function extractIds(val: unknown): string[];
|
|
60
|
+
export declare function refInfoFromAttr(attr: any): {
|
|
61
|
+
ref: string;
|
|
62
|
+
isArray: boolean;
|
|
63
|
+
} | null;
|