@loomcore/api 0.0.1
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/__tests__/common-test.utils.d.ts +35 -0
- package/dist/__tests__/common-test.utils.js +181 -0
- package/dist/__tests__/test-express-app.d.ts +16 -0
- package/dist/__tests__/test-express-app.js +83 -0
- package/dist/config/api-common-config.d.ts +3 -0
- package/dist/config/api-common-config.js +11 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/controllers/api-controller.utils.d.ts +1 -0
- package/dist/controllers/api-controller.utils.js +1 -0
- package/dist/controllers/api.controller.d.ts +22 -0
- package/dist/controllers/api.controller.js +71 -0
- package/dist/controllers/auth.controller.d.ts +16 -0
- package/dist/controllers/auth.controller.js +73 -0
- package/dist/controllers/index.d.ts +1 -0
- package/dist/controllers/index.js +1 -0
- package/dist/errors/bad-request.error.d.ts +9 -0
- package/dist/errors/bad-request.error.js +12 -0
- package/dist/errors/database-connection.error.d.ts +9 -0
- package/dist/errors/database-connection.error.js +12 -0
- package/dist/errors/duplicate-key.error.d.ts +9 -0
- package/dist/errors/duplicate-key.error.js +11 -0
- package/dist/errors/id-not-found.error.d.ts +9 -0
- package/dist/errors/id-not-found.error.js +11 -0
- package/dist/errors/index.d.ts +8 -0
- package/dist/errors/index.js +8 -0
- package/dist/errors/not-found.error.d.ts +9 -0
- package/dist/errors/not-found.error.js +12 -0
- package/dist/errors/server.error.d.ts +9 -0
- package/dist/errors/server.error.js +11 -0
- package/dist/errors/unauthenticated.error.d.ts +8 -0
- package/dist/errors/unauthenticated.error.js +11 -0
- package/dist/errors/unauthorized.error.d.ts +8 -0
- package/dist/errors/unauthorized.error.js +11 -0
- package/dist/middleware/ensure-user-context.d.ts +2 -0
- package/dist/middleware/ensure-user-context.js +7 -0
- package/dist/middleware/error-handler.d.ts +2 -0
- package/dist/middleware/error-handler.js +30 -0
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +3 -0
- package/dist/middleware/is-authenticated.d.ts +2 -0
- package/dist/middleware/is-authenticated.js +27 -0
- package/dist/models/api-common-config.interface.d.ts +22 -0
- package/dist/models/api-common-config.interface.js +1 -0
- package/dist/models/base-api-config.interface.d.ts +12 -0
- package/dist/models/base-api-config.interface.js +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/types/index.d.ts +1 -0
- package/dist/models/types/index.js +1 -0
- package/dist/services/auth.service.d.ts +54 -0
- package/dist/services/auth.service.js +283 -0
- package/dist/services/email.service.d.ts +4 -0
- package/dist/services/email.service.js +24 -0
- package/dist/services/generic-api-service.interface.d.ts +18 -0
- package/dist/services/generic-api-service.interface.js +1 -0
- package/dist/services/generic-api.service.d.ts +44 -0
- package/dist/services/generic-api.service.js +378 -0
- package/dist/services/index.d.ts +8 -0
- package/dist/services/index.js +8 -0
- package/dist/services/jwt.service.d.ts +4 -0
- package/dist/services/jwt.service.js +18 -0
- package/dist/services/multi-tenant-api.service.d.ts +10 -0
- package/dist/services/multi-tenant-api.service.js +31 -0
- package/dist/services/password-reset-token.service.d.ts +8 -0
- package/dist/services/password-reset-token.service.js +20 -0
- package/dist/services/tenant-query-decorator.d.ts +14 -0
- package/dist/services/tenant-query-decorator.js +66 -0
- package/dist/utils/address.utils.d.ts +6 -0
- package/dist/utils/address.utils.js +15 -0
- package/dist/utils/api.utils.d.ts +17 -0
- package/dist/utils/api.utils.js +60 -0
- package/dist/utils/conversion.utils.d.ts +5 -0
- package/dist/utils/conversion.utils.js +14 -0
- package/dist/utils/db.utils.d.ts +27 -0
- package/dist/utils/db.utils.js +273 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/password.utils.d.ts +7 -0
- package/dist/utils/password.utils.js +23 -0
- package/dist/utils/string.utils.d.ts +9 -0
- package/dist/utils/string.utils.js +30 -0
- package/package.json +71 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Db } from 'mongodb';
|
|
2
|
+
import { IUser, IUserContext } from '@loomcore/common/models';
|
|
3
|
+
declare function initialize(database: Db): void;
|
|
4
|
+
declare function createIndexes(db: Db): Promise<void>;
|
|
5
|
+
declare function setupTestUser(): Promise<Partial<IUser>>;
|
|
6
|
+
declare function deleteTestUser(): Promise<null>;
|
|
7
|
+
declare function simulateloginWithTestUser(): Promise<string>;
|
|
8
|
+
declare function getAuthToken(): string;
|
|
9
|
+
declare function verifyToken(token: string): any;
|
|
10
|
+
declare function getTestUser(): Partial<IUser>;
|
|
11
|
+
declare function configureJwtSecret(): void;
|
|
12
|
+
declare function loginWithTestUser(agent: any): Promise<string>;
|
|
13
|
+
declare const testUtils: {
|
|
14
|
+
configureJwtSecret: typeof configureJwtSecret;
|
|
15
|
+
constDeviceIdCookie: string;
|
|
16
|
+
createIndexes: typeof createIndexes;
|
|
17
|
+
deleteTestUser: typeof deleteTestUser;
|
|
18
|
+
getAuthToken: typeof getAuthToken;
|
|
19
|
+
getTestUser: typeof getTestUser;
|
|
20
|
+
initialize: typeof initialize;
|
|
21
|
+
loginWithTestUser: typeof loginWithTestUser;
|
|
22
|
+
newUser1Email: string;
|
|
23
|
+
newUser1Password: string;
|
|
24
|
+
setupTestUser: typeof setupTestUser;
|
|
25
|
+
simulateloginWithTestUser: typeof simulateloginWithTestUser;
|
|
26
|
+
testUserContext: IUserContext;
|
|
27
|
+
testUserId: string;
|
|
28
|
+
testUserEmail: string;
|
|
29
|
+
testUserEmailCaseInsensitive: string;
|
|
30
|
+
testUserPassword: string;
|
|
31
|
+
testOrgId: string;
|
|
32
|
+
testOrgName: string;
|
|
33
|
+
verifyToken: typeof verifyToken;
|
|
34
|
+
};
|
|
35
|
+
export default testUtils;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import jwt from 'jsonwebtoken';
|
|
4
|
+
import { JwtService } from '../services/jwt.service.js';
|
|
5
|
+
import { passwordUtils } from '../utils/password.utils.js';
|
|
6
|
+
import { AuthService } from '../services/auth.service.js';
|
|
7
|
+
let db;
|
|
8
|
+
let collections = {};
|
|
9
|
+
let deviceIdCookie;
|
|
10
|
+
let authService;
|
|
11
|
+
let testUser;
|
|
12
|
+
const JWT_SECRET = 'test-secret';
|
|
13
|
+
const newUser1Email = 'one@test.com';
|
|
14
|
+
const newUser1Password = 'testone';
|
|
15
|
+
const testUserId = '67f33ed5b75090e0dda18a3c';
|
|
16
|
+
const testOrgId = '67e8e19b149f740323af93d7';
|
|
17
|
+
const testOrgName = 'Test Organization';
|
|
18
|
+
const testUserEmail = 'test@example.com';
|
|
19
|
+
const testUserEmailCaseInsensitive = 'tesT@example.com';
|
|
20
|
+
const testUserPassword = 'testPassword';
|
|
21
|
+
const constDeviceIdCookie = crypto.randomBytes(16).toString('hex');
|
|
22
|
+
const testUserContext = {
|
|
23
|
+
user: {
|
|
24
|
+
_id: testUserId,
|
|
25
|
+
email: testUserEmail,
|
|
26
|
+
_created: new Date(),
|
|
27
|
+
_createdBy: 'system',
|
|
28
|
+
_updated: new Date(),
|
|
29
|
+
_updatedBy: 'system'
|
|
30
|
+
},
|
|
31
|
+
_orgId: testOrgId
|
|
32
|
+
};
|
|
33
|
+
function initialize(database) {
|
|
34
|
+
db = database;
|
|
35
|
+
collections = {
|
|
36
|
+
users: db.collection('users'),
|
|
37
|
+
organizations: db.collection('organizations'),
|
|
38
|
+
};
|
|
39
|
+
authService = new AuthService(db);
|
|
40
|
+
}
|
|
41
|
+
async function createIndexes(db) {
|
|
42
|
+
await db.command({
|
|
43
|
+
createIndexes: "users", indexes: [{ key: { email: 1 }, name: 'email_index', unique: true, collation: { locale: 'en', strength: 1 } }]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function setupTestUser() {
|
|
47
|
+
try {
|
|
48
|
+
const result = await deleteTestUser();
|
|
49
|
+
return createTestUser();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.log(error);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function createTestUser() {
|
|
57
|
+
if (!db || !collections.users) {
|
|
58
|
+
throw new Error('Database not initialized. Call initialize() first.');
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const hashedAndSaltedTestUserPassword = await passwordUtils.hashPassword(testUserPassword);
|
|
62
|
+
const existingOrg = await collections.organizations.findOne({ _id: testOrgId });
|
|
63
|
+
if (!existingOrg) {
|
|
64
|
+
const orgInsertResult = await collections.organizations.insertOne({
|
|
65
|
+
_id: new ObjectId(testOrgId),
|
|
66
|
+
name: testOrgName,
|
|
67
|
+
_created: new Date(),
|
|
68
|
+
_createdBy: 'system',
|
|
69
|
+
_updated: new Date(),
|
|
70
|
+
_updatedBy: 'system'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const localTestUser = {
|
|
74
|
+
_id: testUserId,
|
|
75
|
+
email: testUserEmail,
|
|
76
|
+
password: hashedAndSaltedTestUserPassword,
|
|
77
|
+
_orgId: testOrgId,
|
|
78
|
+
_created: new Date(),
|
|
79
|
+
_createdBy: 'system',
|
|
80
|
+
_updated: new Date(),
|
|
81
|
+
_updatedBy: 'system'
|
|
82
|
+
};
|
|
83
|
+
const insertResult = await collections.users.insertOne(localTestUser);
|
|
84
|
+
delete localTestUser['password'];
|
|
85
|
+
testUser = localTestUser;
|
|
86
|
+
return localTestUser;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.log('Error in createTestUser:', error);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function deleteTestUser() {
|
|
94
|
+
let promise = Promise.resolve(null);
|
|
95
|
+
if (testUser) {
|
|
96
|
+
promise = collections.users.deleteOne({ _id: testUser._id });
|
|
97
|
+
}
|
|
98
|
+
return promise;
|
|
99
|
+
}
|
|
100
|
+
async function simulateloginWithTestUser() {
|
|
101
|
+
const req = {
|
|
102
|
+
cookies: {}
|
|
103
|
+
};
|
|
104
|
+
if (deviceIdCookie) {
|
|
105
|
+
req.cookies['deviceId'] = deviceIdCookie;
|
|
106
|
+
}
|
|
107
|
+
const res = {
|
|
108
|
+
cookie: function (name, value) {
|
|
109
|
+
if (name === 'deviceId') {
|
|
110
|
+
deviceIdCookie = value;
|
|
111
|
+
}
|
|
112
|
+
return res;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const loginResponse = await authService.attemptLogin(req, res, testUserEmail, testUserPassword);
|
|
116
|
+
if (!loginResponse?.tokens?.accessToken) {
|
|
117
|
+
throw new Error('Failed to login with test user');
|
|
118
|
+
}
|
|
119
|
+
return `Bearer ${loginResponse.tokens.accessToken}`;
|
|
120
|
+
}
|
|
121
|
+
function getAuthToken() {
|
|
122
|
+
const payload = {
|
|
123
|
+
user: {
|
|
124
|
+
_id: new ObjectId(testUserId),
|
|
125
|
+
email: testUserEmail
|
|
126
|
+
},
|
|
127
|
+
_orgId: testOrgId
|
|
128
|
+
};
|
|
129
|
+
const token = JwtService.sign(payload, JWT_SECRET, { expiresIn: 3600 });
|
|
130
|
+
return `Bearer ${token}`;
|
|
131
|
+
}
|
|
132
|
+
function verifyToken(token) {
|
|
133
|
+
return JwtService.verify(token, JWT_SECRET);
|
|
134
|
+
}
|
|
135
|
+
function getTestUser() {
|
|
136
|
+
return testUser;
|
|
137
|
+
}
|
|
138
|
+
function configureJwtSecret() {
|
|
139
|
+
const originalJwtVerify = jwt.verify;
|
|
140
|
+
jwt.verify = function (token, secret, options) {
|
|
141
|
+
return originalJwtVerify(token, JWT_SECRET, options);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function loginWithTestUser(agent) {
|
|
145
|
+
agent.set('Cookie', [`deviceId=${deviceIdCookie}`]);
|
|
146
|
+
const response = await agent
|
|
147
|
+
.post('/api/auth/login')
|
|
148
|
+
.send({
|
|
149
|
+
email: testUserEmail,
|
|
150
|
+
password: testUserPassword,
|
|
151
|
+
});
|
|
152
|
+
if (!response.body?.data?.tokens?.accessToken) {
|
|
153
|
+
console.error('Login failed:', response.body);
|
|
154
|
+
throw new Error('Failed to login with test user');
|
|
155
|
+
}
|
|
156
|
+
const authorizationHeaderValue = `Bearer ${response.body?.data?.tokens?.accessToken}`;
|
|
157
|
+
return authorizationHeaderValue;
|
|
158
|
+
}
|
|
159
|
+
const testUtils = {
|
|
160
|
+
configureJwtSecret,
|
|
161
|
+
constDeviceIdCookie,
|
|
162
|
+
createIndexes,
|
|
163
|
+
deleteTestUser,
|
|
164
|
+
getAuthToken,
|
|
165
|
+
getTestUser,
|
|
166
|
+
initialize,
|
|
167
|
+
loginWithTestUser,
|
|
168
|
+
newUser1Email,
|
|
169
|
+
newUser1Password,
|
|
170
|
+
setupTestUser,
|
|
171
|
+
simulateloginWithTestUser,
|
|
172
|
+
testUserContext,
|
|
173
|
+
testUserId,
|
|
174
|
+
testUserEmail,
|
|
175
|
+
testUserEmailCaseInsensitive,
|
|
176
|
+
testUserPassword,
|
|
177
|
+
testOrgId,
|
|
178
|
+
testOrgName,
|
|
179
|
+
verifyToken
|
|
180
|
+
};
|
|
181
|
+
export default testUtils;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Application } from 'express';
|
|
2
|
+
import { Db } from 'mongodb';
|
|
3
|
+
export declare class TestExpressApp {
|
|
4
|
+
private static app;
|
|
5
|
+
private static mongoServer;
|
|
6
|
+
private static client;
|
|
7
|
+
private static db;
|
|
8
|
+
static init(): Promise<{
|
|
9
|
+
app: Application;
|
|
10
|
+
db: Db;
|
|
11
|
+
agent: any;
|
|
12
|
+
}>;
|
|
13
|
+
static setupErrorHandling(): Promise<void>;
|
|
14
|
+
static cleanup(): Promise<void>;
|
|
15
|
+
static clearCollections(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import bodyParser from 'body-parser';
|
|
3
|
+
import cookieParser from 'cookie-parser';
|
|
4
|
+
import supertest from 'supertest';
|
|
5
|
+
import { MongoMemoryServer } from 'mongodb-memory-server';
|
|
6
|
+
import { MongoClient } from 'mongodb';
|
|
7
|
+
import { initializeTypeBox } from '@loomcore/common/validation';
|
|
8
|
+
import testUtils from './common-test.utils.js';
|
|
9
|
+
import { setApiCommonConfig } from '../config/api-common-config.js';
|
|
10
|
+
import { errorHandler } from '../middleware/error-handler.js';
|
|
11
|
+
import { ensureUserContext } from '../middleware/ensure-user-context.js';
|
|
12
|
+
export class TestExpressApp {
|
|
13
|
+
static app;
|
|
14
|
+
static mongoServer;
|
|
15
|
+
static client;
|
|
16
|
+
static db;
|
|
17
|
+
static async init() {
|
|
18
|
+
setApiCommonConfig({
|
|
19
|
+
env: 'test',
|
|
20
|
+
hostName: 'localhost',
|
|
21
|
+
appName: 'test-app',
|
|
22
|
+
clientSecret: 'test-secret',
|
|
23
|
+
debug: {
|
|
24
|
+
showErrors: false
|
|
25
|
+
},
|
|
26
|
+
app: { multiTenant: true },
|
|
27
|
+
auth: {
|
|
28
|
+
jwtExpirationInSeconds: 3600,
|
|
29
|
+
refreshTokenExpirationInDays: 7,
|
|
30
|
+
deviceIdCookieMaxAgeInDays: 730,
|
|
31
|
+
passwordResetTokenExpirationInMinutes: 20
|
|
32
|
+
},
|
|
33
|
+
email: {
|
|
34
|
+
sendGridApiKey: 'SG.WeDontHaveAKeyYet',
|
|
35
|
+
fromAddress: undefined
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
initializeTypeBox();
|
|
39
|
+
if (!this.db) {
|
|
40
|
+
this.mongoServer = await MongoMemoryServer.create();
|
|
41
|
+
const uri = this.mongoServer.getUri();
|
|
42
|
+
this.client = await MongoClient.connect(uri);
|
|
43
|
+
this.db = this.client.db();
|
|
44
|
+
testUtils.initialize(this.db);
|
|
45
|
+
await testUtils.createIndexes(this.db);
|
|
46
|
+
}
|
|
47
|
+
if (!this.app) {
|
|
48
|
+
this.app = express();
|
|
49
|
+
this.app.use(bodyParser.json());
|
|
50
|
+
this.app.use(cookieParser());
|
|
51
|
+
this.app.use(ensureUserContext);
|
|
52
|
+
this.app.use((req, res, next) => {
|
|
53
|
+
next();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const agent = supertest.agent(this.app);
|
|
57
|
+
return {
|
|
58
|
+
app: this.app,
|
|
59
|
+
db: this.db,
|
|
60
|
+
agent
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
static async setupErrorHandling() {
|
|
64
|
+
this.app.use(errorHandler);
|
|
65
|
+
}
|
|
66
|
+
static async cleanup() {
|
|
67
|
+
if (this.client) {
|
|
68
|
+
await this.client.close();
|
|
69
|
+
}
|
|
70
|
+
if (this.mongoServer) {
|
|
71
|
+
await this.mongoServer.stop();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
static async clearCollections() {
|
|
75
|
+
if (!this.db) {
|
|
76
|
+
throw new Error('Database not initialized');
|
|
77
|
+
}
|
|
78
|
+
const collections = await this.db.collections();
|
|
79
|
+
for (const collection of collections) {
|
|
80
|
+
await collection.deleteMany({});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export let config;
|
|
2
|
+
let isConfigSet = false;
|
|
3
|
+
export function setApiCommonConfig(apiCommonConfig) {
|
|
4
|
+
if (!isConfigSet) {
|
|
5
|
+
config = apiCommonConfig;
|
|
6
|
+
isConfigSet = true;
|
|
7
|
+
}
|
|
8
|
+
else if (config.env !== 'test') {
|
|
9
|
+
console.warn('ApiCommonConfig data has already been set. Ignoring subsequent calls to setApiCommonConfig.');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './api-common-config.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './api-common-config.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Application, NextFunction, Request, Response } from 'express';
|
|
2
|
+
import { TSchema } from '@sinclair/typebox';
|
|
3
|
+
import { IEntity, IModelSpec } from '@loomcore/common/models';
|
|
4
|
+
import { IGenericApiService } from '../services/index.js';
|
|
5
|
+
export declare abstract class ApiController<T extends IEntity> {
|
|
6
|
+
protected app: Application;
|
|
7
|
+
protected service: IGenericApiService<T>;
|
|
8
|
+
protected slug: string;
|
|
9
|
+
protected apiResourceName: string;
|
|
10
|
+
protected modelSpec?: IModelSpec;
|
|
11
|
+
protected publicSchema?: TSchema;
|
|
12
|
+
protected constructor(slug: string, app: Application, service: IGenericApiService<T>, resourceName?: string, modelSpec?: IModelSpec, publicSchema?: TSchema);
|
|
13
|
+
mapRoutes(app: Application): void;
|
|
14
|
+
getAll(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
15
|
+
get(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
16
|
+
getById(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
17
|
+
getCount(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
18
|
+
create(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
19
|
+
fullUpdateById(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
20
|
+
partialUpdateById(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
21
|
+
deleteById(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isAuthenticated } from '../middleware/index.js';
|
|
2
|
+
import { apiUtils } from '../utils/index.js';
|
|
3
|
+
export class ApiController {
|
|
4
|
+
app;
|
|
5
|
+
service;
|
|
6
|
+
slug;
|
|
7
|
+
apiResourceName;
|
|
8
|
+
modelSpec;
|
|
9
|
+
publicSchema;
|
|
10
|
+
constructor(slug, app, service, resourceName = '', modelSpec, publicSchema) {
|
|
11
|
+
this.slug = slug;
|
|
12
|
+
this.app = app;
|
|
13
|
+
this.service = service;
|
|
14
|
+
this.apiResourceName = resourceName;
|
|
15
|
+
this.modelSpec = modelSpec;
|
|
16
|
+
this.publicSchema = publicSchema;
|
|
17
|
+
this.mapRoutes(app);
|
|
18
|
+
}
|
|
19
|
+
mapRoutes(app) {
|
|
20
|
+
app.get(`/api/${this.slug}`, isAuthenticated, this.get.bind(this));
|
|
21
|
+
app.get(`/api/${this.slug}/all`, isAuthenticated, this.getAll.bind(this));
|
|
22
|
+
app.get(`/api/${this.slug}/count`, isAuthenticated, this.getCount.bind(this));
|
|
23
|
+
app.get(`/api/${this.slug}/:id`, isAuthenticated, this.getById.bind(this));
|
|
24
|
+
app.post(`/api/${this.slug}`, isAuthenticated, this.create.bind(this));
|
|
25
|
+
app.put(`/api/${this.slug}/:id`, isAuthenticated, this.fullUpdateById.bind(this));
|
|
26
|
+
app.patch(`/api/${this.slug}/:id`, isAuthenticated, this.partialUpdateById.bind(this));
|
|
27
|
+
app.delete(`/api/${this.slug}/:id`, isAuthenticated, this.deleteById.bind(this));
|
|
28
|
+
}
|
|
29
|
+
async getAll(req, res, next) {
|
|
30
|
+
res.set('Content-Type', 'application/json');
|
|
31
|
+
const entities = await this.service.getAll(req.userContext);
|
|
32
|
+
apiUtils.apiResponse(res, 200, { data: entities }, this.modelSpec, this.publicSchema);
|
|
33
|
+
}
|
|
34
|
+
async get(req, res, next) {
|
|
35
|
+
res.set('Content-Type', 'application/json');
|
|
36
|
+
const queryOptions = apiUtils.getQueryOptionsFromRequest(req);
|
|
37
|
+
const pagedResult = await this.service.get(req.userContext, queryOptions);
|
|
38
|
+
apiUtils.apiResponse(res, 200, { data: pagedResult }, this.modelSpec, this.publicSchema);
|
|
39
|
+
}
|
|
40
|
+
async getById(req, res, next) {
|
|
41
|
+
let id = req.params?.id;
|
|
42
|
+
res.set('Content-Type', 'application/json');
|
|
43
|
+
const entity = await this.service.getById(req.userContext, id);
|
|
44
|
+
apiUtils.apiResponse(res, 200, { data: entity }, this.modelSpec, this.publicSchema);
|
|
45
|
+
}
|
|
46
|
+
async getCount(req, res, next) {
|
|
47
|
+
res.set('Content-Type', 'application/json');
|
|
48
|
+
const count = await this.service.getCount(req.userContext);
|
|
49
|
+
apiUtils.apiResponse(res, 200, { data: count }, this.modelSpec, this.publicSchema);
|
|
50
|
+
}
|
|
51
|
+
async create(req, res, next) {
|
|
52
|
+
res.set('Content-Type', 'application/json');
|
|
53
|
+
const entity = await this.service.create(req.userContext, req.body);
|
|
54
|
+
apiUtils.apiResponse(res, 201, { data: entity || undefined }, this.modelSpec, this.publicSchema);
|
|
55
|
+
}
|
|
56
|
+
async fullUpdateById(req, res, next) {
|
|
57
|
+
res.set('Content-Type', 'application/json');
|
|
58
|
+
const updateResult = await this.service.fullUpdateById(req.userContext, req.params.id, req.body);
|
|
59
|
+
apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSchema);
|
|
60
|
+
}
|
|
61
|
+
async partialUpdateById(req, res, next) {
|
|
62
|
+
res.set('Content-Type', 'application/json');
|
|
63
|
+
const updateResult = await this.service.partialUpdateById(req.userContext, req.params.id, req.body);
|
|
64
|
+
apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSchema);
|
|
65
|
+
}
|
|
66
|
+
async deleteById(req, res, next) {
|
|
67
|
+
res.set('Content-Type', 'application/json');
|
|
68
|
+
const deleteResult = await this.service.deleteById(req.userContext, req.params.id);
|
|
69
|
+
apiUtils.apiResponse(res, 200, { data: deleteResult }, this.modelSpec, this.publicSchema);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Application, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { Db } from 'mongodb';
|
|
3
|
+
import { AuthService } from '../services/index.js';
|
|
4
|
+
export declare class AuthController {
|
|
5
|
+
authService: AuthService;
|
|
6
|
+
constructor(app: Application, db: Db);
|
|
7
|
+
mapRoutes(app: Application): void;
|
|
8
|
+
login(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
9
|
+
registerUser(req: Request, res: Response): Promise<void>;
|
|
10
|
+
requestTokenUsingRefreshToken(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
11
|
+
getUserContext(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
12
|
+
afterAuth(req: Request, res: Response, loginResponse: any): void;
|
|
13
|
+
changePassword(req: Request, res: Response): Promise<void>;
|
|
14
|
+
forgotPassword(req: Request, res: Response): Promise<void>;
|
|
15
|
+
resetPassword(req: Request, res: Response): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { LoginResponseSpec, TokenResponseSpec, UserSpec, PublicUserSchema, UserContextSpec } from '@loomcore/common/models';
|
|
2
|
+
import { BadRequestError, UnauthenticatedError } from '../errors/index.js';
|
|
3
|
+
import { isAuthenticated } from '../middleware/index.js';
|
|
4
|
+
import { apiUtils } from '../utils/index.js';
|
|
5
|
+
import { AuthService } from '../services/index.js';
|
|
6
|
+
export class AuthController {
|
|
7
|
+
authService;
|
|
8
|
+
constructor(app, db) {
|
|
9
|
+
const authService = new AuthService(db);
|
|
10
|
+
this.authService = authService;
|
|
11
|
+
this.mapRoutes(app);
|
|
12
|
+
}
|
|
13
|
+
mapRoutes(app) {
|
|
14
|
+
app.post(`/api/auth/login`, this.login.bind(this), this.afterAuth.bind(this));
|
|
15
|
+
app.post(`/api/auth/register`, this.registerUser.bind(this));
|
|
16
|
+
app.get(`/api/auth/refresh`, this.requestTokenUsingRefreshToken.bind(this));
|
|
17
|
+
app.get(`/api/auth/get-user-context`, isAuthenticated, this.getUserContext.bind(this));
|
|
18
|
+
app.patch(`/api/auth/change-password`, isAuthenticated, this.changePassword.bind(this));
|
|
19
|
+
app.post(`/api/auth/forgot-password`, this.forgotPassword.bind(this));
|
|
20
|
+
app.post(`/api/auth/reset-password`, this.resetPassword.bind(this));
|
|
21
|
+
}
|
|
22
|
+
async login(req, res, next) {
|
|
23
|
+
const { email, password } = req.body;
|
|
24
|
+
res.set('Content-Type', 'application/json');
|
|
25
|
+
const loginResponse = await this.authService.attemptLogin(req, res, email, password);
|
|
26
|
+
apiUtils.apiResponse(res, 200, { data: loginResponse }, LoginResponseSpec);
|
|
27
|
+
}
|
|
28
|
+
async registerUser(req, res) {
|
|
29
|
+
const userContext = req.userContext;
|
|
30
|
+
const body = req.body;
|
|
31
|
+
const user = await this.authService.createUser(userContext, body);
|
|
32
|
+
apiUtils.apiResponse(res, 201, { data: user || undefined }, UserSpec, PublicUserSchema);
|
|
33
|
+
}
|
|
34
|
+
async requestTokenUsingRefreshToken(req, res, next) {
|
|
35
|
+
let tokens = await this.authService.requestTokenUsingRefreshToken(req);
|
|
36
|
+
if (tokens) {
|
|
37
|
+
apiUtils.apiResponse(res, 200, { data: tokens }, TokenResponseSpec);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
throw new UnauthenticatedError();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async getUserContext(req, res, next) {
|
|
44
|
+
const userContext = req.userContext;
|
|
45
|
+
const clientUserContext = { user: userContext.user };
|
|
46
|
+
apiUtils.apiResponse(res, 200, { data: clientUserContext }, UserContextSpec);
|
|
47
|
+
}
|
|
48
|
+
afterAuth(req, res, loginResponse) {
|
|
49
|
+
console.log('in afterAuth');
|
|
50
|
+
}
|
|
51
|
+
async changePassword(req, res) {
|
|
52
|
+
const userContext = req.userContext;
|
|
53
|
+
const body = req.body;
|
|
54
|
+
const updateResult = await this.authService.changeLoggedInUsersPassword(userContext, body);
|
|
55
|
+
apiUtils.apiResponse(res, 200, { data: updateResult });
|
|
56
|
+
}
|
|
57
|
+
async forgotPassword(req, res) {
|
|
58
|
+
const email = req.body?.email;
|
|
59
|
+
const user = await this.authService.getUserByEmail(email);
|
|
60
|
+
if (user) {
|
|
61
|
+
await this.authService.sendResetPasswordEmail(email);
|
|
62
|
+
}
|
|
63
|
+
apiUtils.apiResponse(res, 200);
|
|
64
|
+
}
|
|
65
|
+
async resetPassword(req, res) {
|
|
66
|
+
const { email, token, password } = req.body;
|
|
67
|
+
if (!email || !token || !password) {
|
|
68
|
+
throw new BadRequestError('Missing required fields: email, token, and password are required.');
|
|
69
|
+
}
|
|
70
|
+
const response = await this.authService.resetPassword(email, token, password);
|
|
71
|
+
apiUtils.apiResponse(res, 200, { data: response });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './api.controller.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './api.controller.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CustomError } from '@loomcore/common/errors';
|
|
2
|
+
export class BadRequestError extends CustomError {
|
|
3
|
+
statusCode = 400;
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
Object.setPrototypeOf(this, BadRequestError.prototype);
|
|
7
|
+
Error.captureStackTrace(this, BadRequestError);
|
|
8
|
+
}
|
|
9
|
+
serializeErrors() {
|
|
10
|
+
return [{ message: this.message }];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CustomError } from '@loomcore/common/errors';
|
|
2
|
+
export class DatabaseConnectionError extends CustomError {
|
|
3
|
+
statusCode = 500;
|
|
4
|
+
reason = 'Error connecting to database';
|
|
5
|
+
constructor() {
|
|
6
|
+
super('Error connecting to database');
|
|
7
|
+
Object.setPrototypeOf(this, DatabaseConnectionError.prototype);
|
|
8
|
+
}
|
|
9
|
+
serializeErrors() {
|
|
10
|
+
return [{ message: this.reason }];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CustomError } from '@loomcore/common/errors';
|
|
2
|
+
export class DuplicateKeyError extends CustomError {
|
|
3
|
+
statusCode = 400;
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
Object.setPrototypeOf(this, DuplicateKeyError.prototype);
|
|
7
|
+
}
|
|
8
|
+
serializeErrors() {
|
|
9
|
+
return [{ message: this.message }];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CustomError } from '@loomcore/common/errors';
|
|
2
|
+
export class IdNotFoundError extends CustomError {
|
|
3
|
+
statusCode = 404;
|
|
4
|
+
constructor() {
|
|
5
|
+
super('Id not found');
|
|
6
|
+
Object.setPrototypeOf(this, IdNotFoundError.prototype);
|
|
7
|
+
}
|
|
8
|
+
serializeErrors() {
|
|
9
|
+
return [{ message: 'Id not found' }];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './bad-request.error.js';
|
|
2
|
+
export * from './database-connection.error.js';
|
|
3
|
+
export * from './duplicate-key.error.js';
|
|
4
|
+
export * from './id-not-found.error.js';
|
|
5
|
+
export * from './not-found.error.js';
|
|
6
|
+
export * from './server.error.js';
|
|
7
|
+
export * from './unauthenticated.error.js';
|
|
8
|
+
export * from './unauthorized.error.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './bad-request.error.js';
|
|
2
|
+
export * from './database-connection.error.js';
|
|
3
|
+
export * from './duplicate-key.error.js';
|
|
4
|
+
export * from './id-not-found.error.js';
|
|
5
|
+
export * from './not-found.error.js';
|
|
6
|
+
export * from './server.error.js';
|
|
7
|
+
export * from './unauthenticated.error.js';
|
|
8
|
+
export * from './unauthorized.error.js';
|