@drax/identity-back 0.0.31 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/IdentityConfig.js +2 -3
- package/dist/factory/RoleServiceFactory.js +8 -7
- package/dist/factory/TenantServiceFactory.js +8 -7
- package/dist/factory/UserApiKeyServiceFactory.js +24 -0
- package/dist/factory/UserServiceFactory.js +8 -7
- package/dist/graphql/resolvers/user-api-key.resolvers.js +89 -0
- package/dist/graphql/resolvers/user.resolvers.js +7 -2
- package/dist/graphql/types/userApiKey.graphql +33 -0
- package/dist/index.js +4 -2
- package/dist/interfaces/IUserApiKeyRepository.js +1 -0
- package/dist/middleware/apiKeyMiddleware.js +30 -0
- package/dist/middleware/rbacMiddleware.js +0 -1
- package/dist/models/UserApiKeyModel.js +44 -0
- package/dist/permissions/IdentityPermissions.js +6 -0
- package/dist/rbac/Rbac.js +8 -0
- package/dist/repository/mongo/UserApiKeyMongoRepository.js +82 -0
- package/dist/repository/mongo/UserMongoRepository.js +3 -3
- package/dist/repository/sqlite/RoleSqliteRepository.js +16 -18
- package/dist/repository/sqlite/TenantSqliteRepository.js +13 -11
- package/dist/repository/sqlite/UserApiKeySqliteRepository.js +147 -0
- package/dist/repository/sqlite/UserSqliteRepository.js +29 -26
- package/dist/routes/UserApiKeyRoutes.js +119 -0
- package/dist/routes/UserRoutes.js +5 -1
- package/dist/services/UserApiKeyService.js +65 -0
- package/dist/setup/LoadIdentityConfigFromEnv.js +3 -3
- package/dist/utils/AuthUtils.js +10 -0
- package/dist/zod/UserApiKeyZod.js +9 -0
- package/package.json +7 -6
- package/src/config/IdentityConfig.ts +4 -3
- package/src/factory/RoleServiceFactory.ts +11 -11
- package/src/factory/TenantServiceFactory.ts +11 -11
- package/src/factory/UserApiKeyServiceFactory.ts +30 -0
- package/src/factory/UserServiceFactory.ts +8 -7
- package/src/graphql/resolvers/tenant.resolvers.ts +0 -1
- package/src/graphql/resolvers/user-api-key.resolvers.ts +94 -0
- package/src/graphql/resolvers/user.resolvers.ts +9 -2
- package/src/graphql/types/userApiKey.graphql +33 -0
- package/src/index.ts +10 -0
- package/src/interfaces/IUserApiKeyRepository.ts +8 -0
- package/src/middleware/apiKeyMiddleware.ts +35 -0
- package/src/middleware/rbacMiddleware.ts +1 -2
- package/src/models/UserApiKeyModel.ts +59 -0
- package/src/permissions/IdentityPermissions.ts +7 -0
- package/src/rbac/Rbac.ts +13 -2
- package/src/repository/mongo/UserApiKeyMongoRepository.ts +114 -0
- package/src/repository/mongo/UserMongoRepository.ts +3 -3
- package/src/repository/sqlite/RoleSqliteRepository.ts +28 -20
- package/src/repository/sqlite/TenantSqliteRepository.ts +25 -11
- package/src/repository/sqlite/UserApiKeySqliteRepository.ts +197 -0
- package/src/repository/sqlite/UserSqliteRepository.ts +37 -27
- package/src/routes/UserApiKeyRoutes.ts +128 -0
- package/src/routes/UserRoutes.ts +5 -1
- package/src/services/UserApiKeyService.ts +86 -0
- package/src/setup/LoadIdentityConfigFromEnv.ts +5 -3
- package/src/utils/AuthUtils.ts +11 -0
- package/src/zod/UserApiKeyZod.ts +15 -0
- package/test/data-obj/apikey/root-mongo-user-apikey.ts +10 -0
- package/test/data-obj/roles/admin-mongo-role.ts +0 -3
- package/test/initializers/RoleMongoInitializer.ts +1 -0
- package/test/initializers/RoleSqliteInitializer.ts +1 -0
- package/test/initializers/UserMongoInitializer.ts +18 -0
- package/test/repository/mongo/user-apikey-mongo-repository.test.ts +73 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/types/factory/RoleServiceFactory.d.ts.map +1 -1
- package/types/factory/TenantServiceFactory.d.ts.map +1 -1
- package/types/factory/UserApiKeyServiceFactory.d.ts.map +1 -0
- package/types/factory/UserServiceFactory.d.ts.map +1 -1
- package/types/graphql/resolvers/tenant.resolvers.d.ts.map +1 -1
- package/types/graphql/resolvers/user-api-key.resolvers.d.ts.map +1 -0
- package/types/graphql/resolvers/user.resolvers.d.ts.map +1 -1
- package/types/index.d.ts.map +1 -1
- package/types/interfaces/IUserApiKeyRepository.d.ts.map +1 -0
- package/types/middleware/apiKeyMiddleware.d.ts.map +1 -0
- package/types/middleware/rbacMiddleware.d.ts.map +1 -1
- package/types/models/UserApiKeyModel.d.ts.map +1 -0
- package/types/rbac/Rbac.d.ts +15 -0
- package/types/rbac/Rbac.d.ts.map +1 -1
- package/types/repository/mongo/UserApiKeyMongoRepository.d.ts.map +1 -0
- package/types/repository/sqlite/RoleSqliteRepository.d.ts.map +1 -1
- package/types/repository/sqlite/TenantSqliteRepository.d.ts.map +1 -1
- package/types/repository/sqlite/UserApiKeySqliteRepository.d.ts +19 -0
- package/types/repository/sqlite/UserApiKeySqliteRepository.d.ts.map +1 -0
- package/types/repository/sqlite/UserSqliteRepository.d.ts.map +1 -1
- package/types/routes/UserApiKeyRoutes.d.ts.map +1 -0
- package/types/routes/UserRoutes.d.ts.map +1 -1
- package/types/services/UserApiKeyService.d.ts.map +1 -0
- package/types/setup/LoadIdentityConfigFromEnv.d.ts.map +1 -1
- package/types/utils/AuthUtils.d.ts.map +1 -1
- package/types/zod/UserApiKeyZod.d.ts +16 -0
- package/types/zod/UserApiKeyZod.d.ts.map +1 -0
- package/src/utils/DbSetupUtils.ts +0 -41
- package/types/config/IdentityConfig.d.ts +0 -13
- package/types/config/IdentityConfig.d.ts.map +0 -1
- package/types/graphql/resolvers/role.resolvers.d.ts +0 -52
- package/types/graphql/resolvers/tenant.resolvers.d.ts +0 -49
- package/types/graphql/resolvers/user.resolvers.d.ts +0 -67
- package/types/utils/DbSetupUtils.d.ts.map +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import sqlite from "better-sqlite3";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { SqliteErrorToValidationError, SqliteTableBuilder } from "@drax/common-back";
|
|
4
|
+
import UserSqliteRepository from "./UserSqliteRepository.js";
|
|
5
|
+
const tableFields = [
|
|
6
|
+
{ name: "secret", type: "TEXT", unique: true, primary: false },
|
|
7
|
+
{ name: "name", type: "TEXT", unique: false, primary: false },
|
|
8
|
+
{ name: "user", type: "TEXT", unique: false, primary: false },
|
|
9
|
+
{ name: "ipv4", type: "TEXT", unique: false, primary: false },
|
|
10
|
+
{ name: "ipv6", type: "TEXT", unique: false, primary: false },
|
|
11
|
+
{ name: "createdAt", type: "TEXT", unique: false, primary: false }
|
|
12
|
+
];
|
|
13
|
+
class UserApiKeySqliteRepository {
|
|
14
|
+
constructor(dataBaseFile, verbose = false) {
|
|
15
|
+
this.dataBaseFile = dataBaseFile;
|
|
16
|
+
this.userRepository = new UserSqliteRepository(dataBaseFile, verbose);
|
|
17
|
+
this.db = new sqlite(dataBaseFile, { verbose: verbose ? console.log : null });
|
|
18
|
+
this.table();
|
|
19
|
+
}
|
|
20
|
+
async findUserById(id) {
|
|
21
|
+
return await this.userRepository.findById(id);
|
|
22
|
+
}
|
|
23
|
+
table() {
|
|
24
|
+
const builder = new SqliteTableBuilder(this.dataBaseFile, 'user_api_keys', tableFields, false);
|
|
25
|
+
builder.build('id');
|
|
26
|
+
}
|
|
27
|
+
async create(userApiKeyData) {
|
|
28
|
+
try {
|
|
29
|
+
if (!userApiKeyData.id) {
|
|
30
|
+
userApiKeyData.id = randomUUID();
|
|
31
|
+
}
|
|
32
|
+
if (userApiKeyData.ipv4 && Array.isArray(userApiKeyData.ipv4) && userApiKeyData.ipv4.length > 0) {
|
|
33
|
+
userApiKeyData.ipv4 = userApiKeyData.ipv4.join(',');
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
userApiKeyData.ipv4 = "";
|
|
37
|
+
}
|
|
38
|
+
if (userApiKeyData.ipv6 && Array.isArray(userApiKeyData.ipv6) && userApiKeyData.ipv6.length > 0) {
|
|
39
|
+
userApiKeyData.ipv6 = userApiKeyData.ipv6.join(',');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
userApiKeyData.ipv6 = "";
|
|
43
|
+
}
|
|
44
|
+
userApiKeyData.createdAt = (new Date().toISOString());
|
|
45
|
+
const fields = Object.keys(userApiKeyData)
|
|
46
|
+
.map(field => `${field}`)
|
|
47
|
+
.join(', ');
|
|
48
|
+
const values = Object.keys(userApiKeyData)
|
|
49
|
+
.map(field => `@${field}`)
|
|
50
|
+
.join(', ');
|
|
51
|
+
const stmt = this.db.prepare(`INSERT INTO user_api_keys (${fields})
|
|
52
|
+
VALUES (${values})`);
|
|
53
|
+
stmt.run(userApiKeyData);
|
|
54
|
+
return this.findById(userApiKeyData.id);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.log(e);
|
|
58
|
+
throw SqliteErrorToValidationError(e, userApiKeyData);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async findById(id) {
|
|
62
|
+
const userApiKey = this.db.prepare('SELECT * FROM user_api_keys WHERE id = ?').get(id);
|
|
63
|
+
userApiKey.ipv4 = userApiKey.ipv4 != "" ? userApiKey.ipv4.split(',') : [];
|
|
64
|
+
userApiKey.ipv6 = userApiKey.ipv6 != "" ? userApiKey.ipv6.split(',') : [];
|
|
65
|
+
userApiKey.user = await this.findUserById(userApiKey.user);
|
|
66
|
+
return userApiKey;
|
|
67
|
+
}
|
|
68
|
+
async findBySecret(secret) {
|
|
69
|
+
const userApiKey = this.db.prepare('SELECT * FROM user_api_keys WHERE secret = ?').get(secret);
|
|
70
|
+
userApiKey.ipv4 = userApiKey.ipv4 != "" ? userApiKey.ipv4.split(',') : [];
|
|
71
|
+
userApiKey.ipv6 = userApiKey.ipv6 != "" ? userApiKey.ipv6.split(',') : [];
|
|
72
|
+
userApiKey.user = await this.findUserById(userApiKey.user);
|
|
73
|
+
return userApiKey;
|
|
74
|
+
}
|
|
75
|
+
async update(id, userApiKeyData) {
|
|
76
|
+
try {
|
|
77
|
+
if (userApiKeyData.ipv4 && Array.isArray(userApiKeyData.ipv4) && userApiKeyData.ipv4.length > 0) {
|
|
78
|
+
userApiKeyData.ipv4 = userApiKeyData.ipv4.join(',');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
userApiKeyData.ipv4 = "";
|
|
82
|
+
}
|
|
83
|
+
if (userApiKeyData.ipv6 && Array.isArray(userApiKeyData.ipv6) && userApiKeyData.ipv6.length > 0) {
|
|
84
|
+
userApiKeyData.ipv6 = userApiKeyData.ipv6.join(',');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
userApiKeyData.ipv6 = "";
|
|
88
|
+
}
|
|
89
|
+
delete userApiKeyData.secret;
|
|
90
|
+
const setClauses = Object.keys(userApiKeyData)
|
|
91
|
+
.map(field => `${field} = @${field}`)
|
|
92
|
+
.join(', ');
|
|
93
|
+
userApiKeyData.id = id;
|
|
94
|
+
const stmt = this.db.prepare(`UPDATE user_api_keys
|
|
95
|
+
SET ${setClauses}
|
|
96
|
+
WHERE id = @id `);
|
|
97
|
+
stmt.run(userApiKeyData);
|
|
98
|
+
return this.findById(id);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
console.log(e);
|
|
102
|
+
throw SqliteErrorToValidationError(e, userApiKeyData);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async delete(id) {
|
|
106
|
+
const stmt = this.db.prepare('DELETE FROM user_api_keys WHERE id = ?');
|
|
107
|
+
stmt.run(id);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
async paginate({ page = 1, limit = 5, orderBy = '', orderDesc = false, search = '', filters = [] }) {
|
|
111
|
+
const offset = page > 1 ? (page - 1) * limit : 0;
|
|
112
|
+
let where = "";
|
|
113
|
+
if (search) {
|
|
114
|
+
where = ` WHERE name LIKE '%${search}%'`;
|
|
115
|
+
}
|
|
116
|
+
let whereFilters = [];
|
|
117
|
+
if (filters && filters.length > 0) {
|
|
118
|
+
where = where ? ` AND ` : ` WHERE `;
|
|
119
|
+
for (const filter of filters) {
|
|
120
|
+
if (['eq', '$eq'].includes(filter.operator)) {
|
|
121
|
+
whereFilters.push(` ${filter.field} = '${filter.value}' `);
|
|
122
|
+
}
|
|
123
|
+
if (['ne', '$ne'].includes(filter.operator)) {
|
|
124
|
+
whereFilters.push(` ${filter.field} != '${filter.value}' `);
|
|
125
|
+
}
|
|
126
|
+
if (['in', '$in'].includes(filter.operator)) {
|
|
127
|
+
whereFilters.push(` ${filter.field} LIKE '%${filter.value}%' `);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
where += whereFilters.join(" AND ");
|
|
131
|
+
}
|
|
132
|
+
const rCount = this.db.prepare('SELECT COUNT(*) as count FROM user_api_keys' + where).get();
|
|
133
|
+
const userApiKeys = this.db.prepare('SELECT * FROM user_api_keys ' + where + ' LIMIT ? OFFSET ?').all([limit, offset]);
|
|
134
|
+
for (const userApiKey of userApiKeys) {
|
|
135
|
+
userApiKey.ipv4 = userApiKey.ipv4 != "" ? userApiKey.ipv4.split(',') : [];
|
|
136
|
+
userApiKey.ipv6 = userApiKey.ipv6 != "" ? userApiKey.ipv6.split(',') : [];
|
|
137
|
+
userApiKey.user = await this.findUserById(userApiKey.user);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
page: page,
|
|
141
|
+
limit: limit,
|
|
142
|
+
total: rCount.count,
|
|
143
|
+
items: userApiKeys
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export default UserApiKeySqliteRepository;
|
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import sqlite from "better-sqlite3";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { SqliteErrorToValidationError, ValidationError } from "@drax/common-back";
|
|
3
|
+
import { SqliteErrorToValidationError, SqliteTableBuilder, ValidationError } from "@drax/common-back";
|
|
4
4
|
import RoleSqliteRepository from "./RoleSqliteRepository.js";
|
|
5
5
|
import TenantSqliteRepository from "./TenantSqliteRepository.js";
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`;
|
|
6
|
+
const tableFields = [
|
|
7
|
+
{ name: "name", type: "TEXT", unique: false, primary: false },
|
|
8
|
+
{ name: "username", type: "TEXT", unique: true, primary: false },
|
|
9
|
+
{ name: "active", type: "INTEGER", unique: false, primary: false },
|
|
10
|
+
{ name: "active", type: "INTEGER", unique: false, primary: false },
|
|
11
|
+
{ name: "password", type: "TEXT", unique: false, primary: false },
|
|
12
|
+
{ name: "email", type: "TEXT", unique: true, primary: false },
|
|
13
|
+
{ name: "phone", type: "TEXT", unique: false, primary: false },
|
|
14
|
+
{ name: "role", type: "TEXT", unique: false, primary: false },
|
|
15
|
+
{ name: "tenant", type: "TEXT", unique: false, primary: false },
|
|
16
|
+
{ name: "groups", type: "TEXT", unique: false, primary: false },
|
|
17
|
+
{ name: "avatar", type: "TEXT", unique: false, primary: false },
|
|
18
|
+
{ name: "createdAt", type: "TEXT", unique: false, primary: false },
|
|
19
|
+
{ name: "updatedAt", type: "TEXT", unique: false, primary: false }
|
|
20
|
+
];
|
|
22
21
|
class UserSqliteRepository {
|
|
23
|
-
constructor(
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
22
|
+
constructor(dataBaseFile, verbose = false) {
|
|
23
|
+
this.dataBaseFile = dataBaseFile;
|
|
24
|
+
this.db = new sqlite(dataBaseFile, { verbose: verbose ? console.log : null });
|
|
25
|
+
this.roleRepository = new RoleSqliteRepository(dataBaseFile, verbose);
|
|
26
|
+
this.tenantRepository = new TenantSqliteRepository(dataBaseFile, verbose);
|
|
27
27
|
this.table();
|
|
28
28
|
}
|
|
29
29
|
table() {
|
|
30
|
-
this.
|
|
30
|
+
const builder = new SqliteTableBuilder(this.dataBaseFile, 'users', tableFields, false);
|
|
31
|
+
builder.build('id');
|
|
31
32
|
}
|
|
32
33
|
normalizeData(userData) {
|
|
33
34
|
if (userData.groups && Array.isArray(userData.groups)) {
|
|
@@ -42,6 +43,7 @@ class UserSqliteRepository {
|
|
|
42
43
|
if (!await this.findRoleById(userData.role)) {
|
|
43
44
|
throw new ValidationError([{ field: 'role', reason: 'validation.notfound', value: userData.role }]);
|
|
44
45
|
}
|
|
46
|
+
userData.createdAt = (new Date().toISOString());
|
|
45
47
|
this.normalizeData(userData);
|
|
46
48
|
try {
|
|
47
49
|
const fields = Object.keys(userData)
|
|
@@ -64,6 +66,7 @@ class UserSqliteRepository {
|
|
|
64
66
|
if (!await this.findRoleById(userData.role)) {
|
|
65
67
|
throw new ValidationError([{ field: 'role', reason: 'validation.notfound', value: userData.role }]);
|
|
66
68
|
}
|
|
69
|
+
userData.updatedAt = (new Date().toISOString());
|
|
67
70
|
this.normalizeData(userData);
|
|
68
71
|
const setClauses = Object.keys(userData)
|
|
69
72
|
.map(field => `${field} = @${field}`)
|
|
@@ -117,19 +120,19 @@ class UserSqliteRepository {
|
|
|
117
120
|
if (filters && filters.length > 0) {
|
|
118
121
|
where = where ? ` AND ` : ` WHERE `;
|
|
119
122
|
for (const filter of filters) {
|
|
120
|
-
if (
|
|
123
|
+
if (['eq', '$eq'].includes(filter.operator)) {
|
|
121
124
|
whereFilters.push(` ${filter.field} = '${filter.value}' `);
|
|
122
125
|
}
|
|
123
|
-
if (
|
|
126
|
+
if (['ne', '$ne'].includes(filter.operator)) {
|
|
124
127
|
whereFilters.push(` ${filter.field} != '${filter.value}' `);
|
|
125
128
|
}
|
|
126
|
-
if (
|
|
129
|
+
if (['in', '$in'].includes(filter.operator)) {
|
|
127
130
|
whereFilters.push(` ${filter.field} LIKE '%${filter.value}%' `);
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
where += whereFilters.join(" AND ");
|
|
131
134
|
}
|
|
132
|
-
|
|
135
|
+
console.log("paginate where ", where, "search", search, "filters", filters, "whereFilters", whereFilters);
|
|
133
136
|
const rCount = this.db.prepare('SELECT COUNT(*) as count FROM users' + where).get();
|
|
134
137
|
const users = this.db.prepare('SELECT * FROM users' + where + ' LIMIT ? OFFSET ?').all([limit, offset]);
|
|
135
138
|
for (const user of users) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import UserApiKeyServiceFactory from "../factory/UserApiKeyServiceFactory.js";
|
|
2
|
+
import { ValidationError } from "@drax/common-back";
|
|
3
|
+
import { IdentityPermissions } from "../permissions/IdentityPermissions.js";
|
|
4
|
+
import UnauthorizedError from "../errors/UnauthorizedError.js";
|
|
5
|
+
async function UserApiKeyRoutes(fastify, options) {
|
|
6
|
+
fastify.get('/api/user-api-keys', async (request, reply) => {
|
|
7
|
+
try {
|
|
8
|
+
request.rbac.assertAuthenticated();
|
|
9
|
+
request.rbac.assertOrPermissions([
|
|
10
|
+
IdentityPermissions.ViewUserApiKey,
|
|
11
|
+
IdentityPermissions.ViewMyUserApiKey
|
|
12
|
+
]);
|
|
13
|
+
const filters = [];
|
|
14
|
+
if (!request.rbac.hasPermission(IdentityPermissions.ViewUserApiKey)) {
|
|
15
|
+
filters.push({ field: "user", operator: "eq", value: request.rbac.authUser.id });
|
|
16
|
+
}
|
|
17
|
+
const page = request.query.page;
|
|
18
|
+
const limit = request.query.limit;
|
|
19
|
+
const orderBy = request.query.orderBy;
|
|
20
|
+
const orderDesc = request.query.orderDesc;
|
|
21
|
+
const search = request.query.search;
|
|
22
|
+
const userApiKeyService = UserApiKeyServiceFactory();
|
|
23
|
+
let paginateResult = await userApiKeyService.paginate({ page, limit, orderBy, orderDesc, search, filters });
|
|
24
|
+
return paginateResult;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
console.log("/api/user-api-keys", e);
|
|
28
|
+
if (e instanceof ValidationError) {
|
|
29
|
+
reply.statusCode = e.statusCode;
|
|
30
|
+
reply.send({ error: e.message, inputErrors: e.errors });
|
|
31
|
+
}
|
|
32
|
+
else if (e instanceof UnauthorizedError) {
|
|
33
|
+
reply.statusCode = e.statusCode;
|
|
34
|
+
reply.send({ error: e.message });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
reply.statusCode = 500;
|
|
38
|
+
reply.send({ error: 'error.server' });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
fastify.post('/api/user-api-keys', async (request, reply) => {
|
|
43
|
+
try {
|
|
44
|
+
request.rbac.assertPermission(IdentityPermissions.CreateUser);
|
|
45
|
+
const payload = request.body;
|
|
46
|
+
payload.user = request.rbac.authUser.id;
|
|
47
|
+
const userApiKeyService = UserApiKeyServiceFactory();
|
|
48
|
+
let userApiKey = await userApiKeyService.create(payload);
|
|
49
|
+
return userApiKey;
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (e instanceof ValidationError) {
|
|
53
|
+
reply.statusCode = e.statusCode;
|
|
54
|
+
reply.send({ error: e.message, inputErrors: e.errors });
|
|
55
|
+
}
|
|
56
|
+
else if (e instanceof UnauthorizedError) {
|
|
57
|
+
reply.statusCode = e.statusCode;
|
|
58
|
+
reply.send({ error: e.message });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
reply.statusCode = 500;
|
|
62
|
+
reply.send({ error: 'error.server' });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
fastify.put('/api/user-api-keys/:id', async (request, reply) => {
|
|
67
|
+
try {
|
|
68
|
+
request.rbac.assertPermission(IdentityPermissions.UpdateUser);
|
|
69
|
+
const id = request.params.id;
|
|
70
|
+
const payload = request.body;
|
|
71
|
+
const userApiKeyService = UserApiKeyServiceFactory();
|
|
72
|
+
let userApiKey = await userApiKeyService.update(id, payload);
|
|
73
|
+
return userApiKey;
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
if (e instanceof ValidationError) {
|
|
77
|
+
reply.statusCode = e.statusCode;
|
|
78
|
+
reply.send({ error: e.message, inputErrors: e.errors });
|
|
79
|
+
}
|
|
80
|
+
if (e instanceof UnauthorizedError) {
|
|
81
|
+
reply.statusCode = e.statusCode;
|
|
82
|
+
reply.send({ error: e.message });
|
|
83
|
+
}
|
|
84
|
+
else if (e instanceof UnauthorizedError) {
|
|
85
|
+
reply.statusCode = e.statusCode;
|
|
86
|
+
reply.send({ error: e.message });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
reply.statusCode = 500;
|
|
90
|
+
reply.send({ error: 'error.server' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
fastify.delete('/api/user-api-keys/:id', async (request, reply) => {
|
|
95
|
+
try {
|
|
96
|
+
request.rbac.assertPermission(IdentityPermissions.DeleteUser);
|
|
97
|
+
const id = request.params.id;
|
|
98
|
+
const userApiKeyService = UserApiKeyServiceFactory();
|
|
99
|
+
let r = await userApiKeyService.delete(id);
|
|
100
|
+
return r;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (e instanceof ValidationError) {
|
|
104
|
+
reply.statusCode = e.statusCode;
|
|
105
|
+
reply.send({ error: e.message, inputErrors: e.errors });
|
|
106
|
+
}
|
|
107
|
+
else if (e instanceof UnauthorizedError) {
|
|
108
|
+
reply.statusCode = e.statusCode;
|
|
109
|
+
reply.send({ error: e.message });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
reply.statusCode = 500;
|
|
113
|
+
reply.send({ error: 'error.server' });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export default UserApiKeyRoutes;
|
|
119
|
+
export { UserApiKeyRoutes };
|
|
@@ -26,6 +26,7 @@ async function UserRoutes(fastify, options) {
|
|
|
26
26
|
if (request.authUser) {
|
|
27
27
|
const userService = UserServiceFactory();
|
|
28
28
|
let user = await userService.findById(request.authUser.id);
|
|
29
|
+
user.password = undefined;
|
|
29
30
|
delete user.password;
|
|
30
31
|
return user;
|
|
31
32
|
}
|
|
@@ -62,10 +63,13 @@ async function UserRoutes(fastify, options) {
|
|
|
62
63
|
filters.push({ field: 'tenant', operator: '$eq', value: request.rbac.getAuthUser.tenantId });
|
|
63
64
|
}
|
|
64
65
|
let paginateResult = await userService.paginate({ page, limit, orderBy, orderDesc, search, filters });
|
|
66
|
+
for (let item of paginateResult.items) {
|
|
67
|
+
item.password = undefined;
|
|
68
|
+
delete item.password;
|
|
69
|
+
}
|
|
65
70
|
return paginateResult;
|
|
66
71
|
}
|
|
67
72
|
catch (e) {
|
|
68
|
-
console.log("/api/users", e);
|
|
69
73
|
if (e instanceof ValidationError) {
|
|
70
74
|
reply.statusCode = e.statusCode;
|
|
71
75
|
reply.send({ error: e.message, inputErrors: e.errors });
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { DraxConfig, ZodErrorToValidationError } from "@drax/common-back";
|
|
2
|
+
import { userApiKeySchema } from "../zod/UserApiKeyZod.js";
|
|
3
|
+
import { ZodError } from "zod";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import AuthUtils from "../utils/AuthUtils.js";
|
|
6
|
+
import IdentityConfig from "../config/IdentityConfig.js";
|
|
7
|
+
class UserApiKeyService {
|
|
8
|
+
constructor(userApiKeyRepostitory) {
|
|
9
|
+
this._repository = userApiKeyRepostitory;
|
|
10
|
+
console.log("UserApiKeyService constructor");
|
|
11
|
+
}
|
|
12
|
+
async create(userApiKeyData) {
|
|
13
|
+
try {
|
|
14
|
+
userApiKeyData.name = userApiKeyData?.name?.trim();
|
|
15
|
+
const secret = crypto.randomUUID();
|
|
16
|
+
const APIKEY_SECRET = DraxConfig.getOrLoad(IdentityConfig.ApiKeySecret);
|
|
17
|
+
userApiKeyData.secret = AuthUtils.generateHMAC(APIKEY_SECRET, secret);
|
|
18
|
+
await userApiKeySchema.parseAsync(userApiKeyData);
|
|
19
|
+
const userApiKey = await this._repository.create(userApiKeyData);
|
|
20
|
+
userApiKey.secret = secret;
|
|
21
|
+
return userApiKey;
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e instanceof ZodError) {
|
|
25
|
+
throw ZodErrorToValidationError(e, userApiKeyData);
|
|
26
|
+
}
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async update(id, userApiKeyData) {
|
|
31
|
+
try {
|
|
32
|
+
userApiKeyData.name = userApiKeyData?.name?.trim();
|
|
33
|
+
delete userApiKeyData.secret;
|
|
34
|
+
await userApiKeySchema.parseAsync(userApiKeyData);
|
|
35
|
+
const userApiKey = await this._repository.update(id, userApiKeyData);
|
|
36
|
+
return userApiKey;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (e instanceof ZodError) {
|
|
40
|
+
throw ZodErrorToValidationError(e, userApiKeyData);
|
|
41
|
+
}
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async delete(id) {
|
|
46
|
+
const currentUserApiKey = await this.findById(id);
|
|
47
|
+
const deletedUserApiKey = await this._repository.delete(id);
|
|
48
|
+
return deletedUserApiKey;
|
|
49
|
+
}
|
|
50
|
+
async findById(id) {
|
|
51
|
+
const userApiKey = await this._repository.findById(id);
|
|
52
|
+
return userApiKey;
|
|
53
|
+
}
|
|
54
|
+
async findBySecret(secret) {
|
|
55
|
+
const APIKEY_SECRET = DraxConfig.getOrLoad(IdentityConfig.ApiKeySecret);
|
|
56
|
+
const hashedSecret = AuthUtils.generateHMAC(APIKEY_SECRET, secret);
|
|
57
|
+
const userApiKey = await this._repository.findBySecret(hashedSecret);
|
|
58
|
+
return userApiKey;
|
|
59
|
+
}
|
|
60
|
+
async paginate({ page = 1, limit = 5, orderBy = '', orderDesc = false, search = '', filters = [] }) {
|
|
61
|
+
const pagination = await this._repository.paginate({ page, limit, orderBy, orderDesc, search, filters });
|
|
62
|
+
return pagination;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export default UserApiKeyService;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { DraxConfig } from "@drax/common-back";
|
|
2
2
|
import IdentityConfig from "../config/IdentityConfig.js";
|
|
3
3
|
function LoadIdentityConfigFromEnv() {
|
|
4
|
-
DraxConfig.set(IdentityConfig.DbEngine, process.env[IdentityConfig.DbEngine]);
|
|
5
|
-
DraxConfig.set(IdentityConfig.SqliteDbFile, process.env[IdentityConfig.SqliteDbFile]);
|
|
6
|
-
DraxConfig.set(IdentityConfig.MongoDbUri, process.env[IdentityConfig.MongoDbUri]);
|
|
7
4
|
DraxConfig.set(IdentityConfig.JwtSecret, process.env[IdentityConfig.JwtSecret]);
|
|
8
5
|
DraxConfig.set(IdentityConfig.JwtExpiration, process.env[IdentityConfig.JwtExpiration]);
|
|
9
6
|
DraxConfig.set(IdentityConfig.JwtIssuer, process.env[IdentityConfig.JwtIssuer]);
|
|
7
|
+
DraxConfig.set(IdentityConfig.ApiKeySecret, process.env[IdentityConfig.ApiKeySecret]);
|
|
8
|
+
DraxConfig.set(IdentityConfig.RbacCacheTTL, process.env[IdentityConfig.RbacCacheTTL]);
|
|
9
|
+
DraxConfig.set(IdentityConfig.AvatarDir, process.env[IdentityConfig.AvatarDir]);
|
|
10
10
|
}
|
|
11
11
|
export default LoadIdentityConfigFromEnv;
|
|
12
12
|
export { LoadIdentityConfigFromEnv };
|
package/dist/utils/AuthUtils.js
CHANGED
|
@@ -2,6 +2,7 @@ import bcryptjs from "bcryptjs";
|
|
|
2
2
|
import jsonwebtoken from "jsonwebtoken";
|
|
3
3
|
import { DraxConfig } from "@drax/common-back";
|
|
4
4
|
import IdentityConfig from "../config/IdentityConfig.js";
|
|
5
|
+
import crypto from "crypto";
|
|
5
6
|
class AuthUtils {
|
|
6
7
|
static verifyToken(token) {
|
|
7
8
|
const JWT_SECRET = DraxConfig.getOrLoad(IdentityConfig.JwtSecret);
|
|
@@ -51,5 +52,14 @@ class AuthUtils {
|
|
|
51
52
|
let token = jsonwebtoken.sign(payload, JWT_SECRET, options);
|
|
52
53
|
return token;
|
|
53
54
|
}
|
|
55
|
+
static generateHMAC(secret, apikey) {
|
|
56
|
+
// Crear un objeto HMAC utilizando el algoritmo SHA-256 y el secreto
|
|
57
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
58
|
+
// Actualizar el HMAC con la apikey
|
|
59
|
+
hmac.update(apikey);
|
|
60
|
+
// Generar el hash en formato hexadecimal
|
|
61
|
+
return hmac.digest('hex');
|
|
62
|
+
}
|
|
54
63
|
}
|
|
55
64
|
export default AuthUtils;
|
|
65
|
+
export { AuthUtils };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { array, object, string } from "zod";
|
|
2
|
+
const userApiKeySchema = object({
|
|
3
|
+
name: string({ required_error: "validation.required" })
|
|
4
|
+
.min(1, "validation.required"),
|
|
5
|
+
ipv4: array(string().ip({ version: "v4", message: 'validation.invalidIpv4' })),
|
|
6
|
+
ipv6: array(string().ip({ version: "v6", message: 'validation.invalidIpv6' })),
|
|
7
|
+
});
|
|
8
|
+
export default userApiKeySchema;
|
|
9
|
+
export { userApiKeySchema };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.0
|
|
6
|
+
"version": "0.1.0",
|
|
7
7
|
"description": "Identity module for user management, authentication and authorization.",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "types/index.d.ts",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"tsc": "tsc -b tsconfig.json",
|
|
17
17
|
"test": "node --import tsx --test test/**/*",
|
|
18
18
|
"testMongoRepositoryRole": "node --import tsx --test test/repository/mongo/role*",
|
|
19
|
-
"testMongoRepositoryUser": "node --import tsx --test test/repository/mongo/user*",
|
|
19
|
+
"testMongoRepositoryUser": "node --import tsx --test test/repository/mongo/user-mongo*",
|
|
20
|
+
"testMongoRepositoryUserApiKey": "node --import tsx --test test/repository/mongo/user-apikey-mongo*",
|
|
20
21
|
"testSqliteRepositoryUser": "node --import tsx --test test/repository/sqlite/user*",
|
|
21
22
|
"testSqliteRepositoryRole": "node --import tsx --test test/repository/sqlite/role*",
|
|
22
23
|
"testServiceRole": "node --import tsx --test test/service/role*",
|
|
@@ -26,9 +27,9 @@
|
|
|
26
27
|
"author": "Cristian Incarnato & Drax Team",
|
|
27
28
|
"license": "ISC",
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@drax/common-back": "^0.0
|
|
30
|
-
"@drax/common-share": "^0.0
|
|
31
|
-
"@drax/identity-share": "^0.0
|
|
30
|
+
"@drax/common-back": "^0.1.0",
|
|
31
|
+
"@drax/common-share": "^0.1.0",
|
|
32
|
+
"@drax/identity-share": "^0.1.0",
|
|
32
33
|
"bcryptjs": "^2.4.3",
|
|
33
34
|
"express-jwt": "^8.4.1",
|
|
34
35
|
"graphql": "^16.8.2",
|
|
@@ -59,5 +60,5 @@
|
|
|
59
60
|
"debug": "0"
|
|
60
61
|
}
|
|
61
62
|
},
|
|
62
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "bec2a22554155ef5deab5a55245878a4449932ad"
|
|
63
64
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
enum IdentityConfig {
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
SqliteDbFile = "DRAX_SQLITE_FILE",
|
|
5
|
-
MongoDbUri = "DRAX_MONGO_URI",
|
|
3
|
+
|
|
6
4
|
|
|
7
5
|
JwtSecret = "DRAX_JWT_SECRET",
|
|
8
6
|
JwtExpiration = "DRAX_JWT_EXPIRATION",
|
|
9
7
|
JwtIssuer = "DRAX_JWT_ISSUER",
|
|
10
8
|
|
|
9
|
+
ApiKeySecret = "DRAX_APIKEY_SECRET",
|
|
10
|
+
ApiKeyCacheTTL = "DRAX_APIKEY_CACHE_TTL",
|
|
11
|
+
|
|
11
12
|
RbacCacheTTL = "DRAX_RBAC_CACHE_TTL",
|
|
12
13
|
|
|
13
14
|
AvatarDir = "DRAX_AVATAR_DIR",
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import {DraxConfig} from "@drax/common-back"
|
|
1
|
+
import {DraxConfig, CommonConfig, COMMON} from "@drax/common-back"
|
|
2
2
|
import RoleService from "../services/RoleService.js";
|
|
3
3
|
import RoleMongoRepository from "../repository/mongo/RoleMongoRepository.js";
|
|
4
4
|
import RoleSqliteRepository from "../repository/sqlite/RoleSqliteRepository.js";
|
|
5
|
-
import {DbSetupUtils, DbEngine} from "../utils/DbSetupUtils.js";
|
|
6
5
|
import type {IRoleRepository} from "../interfaces/IRoleRepository";
|
|
7
6
|
|
|
8
7
|
let roleService: RoleService
|
|
9
8
|
|
|
10
|
-
const RoleServiceFactory = (verbose: boolean = false)
|
|
9
|
+
const RoleServiceFactory = (verbose: boolean = false): RoleService => {
|
|
11
10
|
|
|
12
|
-
if(!roleService){
|
|
11
|
+
if (!roleService) {
|
|
13
12
|
let roleRepository: IRoleRepository
|
|
14
13
|
|
|
15
|
-
switch (
|
|
16
|
-
case
|
|
17
|
-
console.log("RoleServiceFactory DB ENGINE MONGODB")
|
|
14
|
+
switch (DraxConfig.getOrLoad(CommonConfig.DbEngine)) {
|
|
15
|
+
case COMMON.DB_ENGINES.MONGODB:
|
|
18
16
|
roleRepository = new RoleMongoRepository()
|
|
19
17
|
break;
|
|
20
|
-
case
|
|
21
|
-
|
|
22
|
-
roleRepository = new RoleSqliteRepository(
|
|
18
|
+
case COMMON.DB_ENGINES.SQLITE:
|
|
19
|
+
const dbFile = DraxConfig.getOrLoad(CommonConfig.SqliteDbFile)
|
|
20
|
+
roleRepository = new RoleSqliteRepository(dbFile, verbose)
|
|
23
21
|
break;
|
|
22
|
+
default:
|
|
23
|
+
throw new Error("DraxConfig.DB_ENGINE must be one of " + Object.values(COMMON.DB_ENGINES).join(", "));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
roleService = new RoleService(roleRepository)
|
|
@@ -29,4 +29,4 @@ const RoleServiceFactory = (verbose: boolean = false) : RoleService => {
|
|
|
29
29
|
return roleService
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
export default RoleServiceFactory
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import {DraxConfig} from "@drax/common-back"
|
|
1
|
+
import {COMMON, CommonConfig, DraxConfig} from "@drax/common-back"
|
|
2
2
|
import TenantService from "../services/TenantService.js";
|
|
3
3
|
import TenantMongoRepository from "../repository/mongo/TenantMongoRepository.js";
|
|
4
4
|
import TenantSqliteRepository from "../repository/sqlite/TenantSqliteRepository.js";
|
|
5
|
-
import {DbSetupUtils, DbEngine} from "../utils/DbSetupUtils.js";
|
|
6
5
|
import type {ITenantRepository} from "../interfaces/ITenantRepository";
|
|
7
6
|
|
|
8
7
|
let tenantService: TenantService
|
|
9
8
|
|
|
10
|
-
const TenantServiceFactory = (verbose: boolean = false)
|
|
9
|
+
const TenantServiceFactory = (verbose: boolean = false): TenantService => {
|
|
11
10
|
|
|
12
|
-
if(!tenantService){
|
|
11
|
+
if (!tenantService) {
|
|
13
12
|
let tenantRepository: ITenantRepository
|
|
14
13
|
|
|
15
|
-
switch (
|
|
16
|
-
case
|
|
17
|
-
console.log("TenantServiceFactory DB ENGINE MONGODB")
|
|
14
|
+
switch (DraxConfig.getOrLoad(CommonConfig.DbEngine)) {
|
|
15
|
+
case COMMON.DB_ENGINES.MONGODB:
|
|
18
16
|
tenantRepository = new TenantMongoRepository()
|
|
19
17
|
break;
|
|
20
|
-
case
|
|
21
|
-
|
|
22
|
-
tenantRepository = new TenantSqliteRepository(
|
|
18
|
+
case COMMON.DB_ENGINES.SQLITE:
|
|
19
|
+
const dbFile = DraxConfig.getOrLoad(CommonConfig.SqliteDbFile)
|
|
20
|
+
tenantRepository = new TenantSqliteRepository(dbFile, verbose)
|
|
23
21
|
break;
|
|
22
|
+
default:
|
|
23
|
+
throw new Error("DraxConfig.DB_ENGINE must be one of " + Object.values(COMMON.DB_ENGINES).join(", "));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
tenantService = new TenantService(tenantRepository)
|
|
@@ -29,4 +29,4 @@ const TenantServiceFactory = (verbose: boolean = false) : TenantService => {
|
|
|
29
29
|
return tenantService
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
export default TenantServiceFactory
|