@devopsplaybook.io/common-utils 1.3.0-beta.12.165a8d0 → 1.4.0-beta.13.4edad2b
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/AGENTS.md +9 -0
- package/README.md +44 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/src/Auth.d.ts +34 -0
- package/dist/src/Auth.js +172 -0
- package/dist/src/User.d.ts +25 -0
- package/dist/src/User.js +61 -0
- package/dist/src/UserPassword.d.ts +4 -0
- package/dist/src/UserPassword.js +45 -0
- package/dist/src/UserSession.d.ts +8 -0
- package/dist/src/UserSession.js +2 -0
- package/dist/src/UsersData.d.ts +15 -0
- package/dist/src/UsersData.js +117 -0
- package/dist/src/UsersRoutes.d.ts +13 -0
- package/dist/src/UsersRoutes.js +205 -0
- package/index.ts +6 -0
- package/package.json +10 -2
- package/src/Auth.ts +165 -0
- package/src/User.ts +78 -0
- package/src/UserPassword.spec.ts +28 -0
- package/src/UserPassword.ts +20 -0
- package/src/UserSession.ts +9 -0
- package/src/UsersData.ts +142 -0
- package/src/UsersRoutes.ts +292 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UsersRoutes = void 0;
|
|
4
|
+
const Auth_1 = require("./Auth");
|
|
5
|
+
const User_1 = require("./User");
|
|
6
|
+
const UserPassword_1 = require("./UserPassword");
|
|
7
|
+
const UsersData_1 = require("./UsersData");
|
|
8
|
+
/**
|
|
9
|
+
* Retrieves the OTel span attached to the request by the
|
|
10
|
+
* `@devopsplaybook.io/otel-utils-fastify` hooks.
|
|
11
|
+
*/
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
function requestSpan(req) {
|
|
14
|
+
return req === null || req === void 0 ? void 0 : req.tracerSpanApi;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Standard user management routes shared across applications:
|
|
18
|
+
* initialization status, login (session), user CRUD and password changes.
|
|
19
|
+
*
|
|
20
|
+
* Register on a fastify instance:
|
|
21
|
+
* ```ts
|
|
22
|
+
* fastify.register(new UsersRoutes().getRoutes, { prefix: "/api/users" });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
class UsersRoutes {
|
|
26
|
+
//
|
|
27
|
+
async getRoutes(fastify) {
|
|
28
|
+
//
|
|
29
|
+
fastify.get("/status/initialization", async (req, res) => {
|
|
30
|
+
if ((await (0, UsersData_1.UsersDataList)(requestSpan(req))).length === 0) {
|
|
31
|
+
res.status(201).send({ initialized: false });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
res.status(201).send({ initialized: true });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
fastify.post("/session", async (req, res) => {
|
|
38
|
+
let user;
|
|
39
|
+
// From token
|
|
40
|
+
const userSession = await (0, Auth_1.AuthGetUserSession)(req);
|
|
41
|
+
if (userSession.isAuthenticated) {
|
|
42
|
+
// isAuthenticated implies userId is set
|
|
43
|
+
user = await (0, UsersData_1.UsersDataGet)(requestSpan(req), userSession.userId);
|
|
44
|
+
if (!user) {
|
|
45
|
+
return res.status(403).send({ error: "Authentication Failed" });
|
|
46
|
+
}
|
|
47
|
+
return res.status(201).send({
|
|
48
|
+
success: true,
|
|
49
|
+
token: await (0, Auth_1.AuthGenerateJWT)(user),
|
|
50
|
+
user: user.toTransportJson(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// From User/Pass
|
|
54
|
+
if (!req.body.name) {
|
|
55
|
+
return res.status(400).send({ error: "Missing: Name" });
|
|
56
|
+
}
|
|
57
|
+
if (!req.body.password) {
|
|
58
|
+
return res.status(400).send({ error: "Missing: Password" });
|
|
59
|
+
}
|
|
60
|
+
user = await (0, UsersData_1.UsersDataGetByName)(requestSpan(req), req.body.name);
|
|
61
|
+
if (!user) {
|
|
62
|
+
return res.status(403).send({ error: "Authentication Failed" });
|
|
63
|
+
}
|
|
64
|
+
else if (await (0, UserPassword_1.UserPasswordCheckPassword)(requestSpan(req), user, req.body.password)) {
|
|
65
|
+
return res.status(201).send({
|
|
66
|
+
success: true,
|
|
67
|
+
token: await (0, Auth_1.AuthGenerateJWT)(user),
|
|
68
|
+
user: user.toTransportJson(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return res.status(403).send({ error: "Authentication Failed" });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// ==================== LIST USERS (Admin only) ====================
|
|
76
|
+
fastify.get("/", async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
await (0, Auth_1.AuthMustBeAdmin)(req, res);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const users = await (0, UsersData_1.UsersDataList)(requestSpan(req));
|
|
84
|
+
return res.status(200).send(users.map((u) => u.toTransportJson()));
|
|
85
|
+
});
|
|
86
|
+
fastify.post("/", async (req, res) => {
|
|
87
|
+
const context = requestSpan(req);
|
|
88
|
+
let isInitialized = true;
|
|
89
|
+
if ((await (0, UsersData_1.UsersDataList)(context)).length === 0) {
|
|
90
|
+
isInitialized = false;
|
|
91
|
+
}
|
|
92
|
+
// If initialized, only admin can create users
|
|
93
|
+
if (isInitialized) {
|
|
94
|
+
try {
|
|
95
|
+
await (0, Auth_1.AuthMustBeAdmin)(req, res);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!req.body.name) {
|
|
102
|
+
return res.status(400).send({ error: "Missing: Name" });
|
|
103
|
+
}
|
|
104
|
+
if (!req.body.password) {
|
|
105
|
+
return res.status(400).send({ error: "Missing: Password" });
|
|
106
|
+
}
|
|
107
|
+
if (await (0, UsersData_1.UsersDataGetByName)(context, req.body.name)) {
|
|
108
|
+
return res.status(400).send({ error: "Username Already Exists" });
|
|
109
|
+
}
|
|
110
|
+
const newUser = new User_1.User();
|
|
111
|
+
newUser.name = req.body.name;
|
|
112
|
+
// First user is always admin
|
|
113
|
+
if (isInitialized) {
|
|
114
|
+
newUser.role = req.body.role === "admin" ? "admin" : "user";
|
|
115
|
+
if (req.body.scopes && Array.isArray(req.body.scopes)) {
|
|
116
|
+
newUser.scopes = req.body.scopes.filter((s) => User_1.User.ALL_SCOPES.includes(s));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
newUser.role = "admin";
|
|
121
|
+
newUser.scopes = [...User_1.User.ALL_SCOPES];
|
|
122
|
+
}
|
|
123
|
+
await (0, UserPassword_1.UserPasswordSetPassword)(context, newUser, req.body.password);
|
|
124
|
+
await (0, UsersData_1.UsersDataAdd)(context, newUser);
|
|
125
|
+
res.status(201).send({ user: newUser.toTransportJson() });
|
|
126
|
+
});
|
|
127
|
+
fastify.put("/password", async (req, res) => {
|
|
128
|
+
const context = requestSpan(req);
|
|
129
|
+
const userSession = await (0, Auth_1.AuthGetUserSession)(req);
|
|
130
|
+
if (!userSession.isAuthenticated) {
|
|
131
|
+
return res.status(403).send({ error: "Access Denied" });
|
|
132
|
+
}
|
|
133
|
+
// isAuthenticated implies userId is set
|
|
134
|
+
const user = await (0, UsersData_1.UsersDataGet)(context, userSession.userId);
|
|
135
|
+
if (!user) {
|
|
136
|
+
return res.status(403).send({ error: "Access Denied" });
|
|
137
|
+
}
|
|
138
|
+
if (!req.body.password) {
|
|
139
|
+
return res.status(400).send({ error: "Missing: Password" });
|
|
140
|
+
}
|
|
141
|
+
if (!(await (0, UserPassword_1.UserPasswordCheckPassword)(context, user, req.body.passwordOld))) {
|
|
142
|
+
return res.status(403).send({ error: "Old Password Wrong" });
|
|
143
|
+
}
|
|
144
|
+
await (0, UserPassword_1.UserPasswordSetPassword)(context, user, req.body.password);
|
|
145
|
+
await (0, UsersData_1.UsersDataUpdatePassword)(context, user);
|
|
146
|
+
res.status(201).send({});
|
|
147
|
+
});
|
|
148
|
+
fastify.put("/:id", async (req, res) => {
|
|
149
|
+
const context = requestSpan(req);
|
|
150
|
+
try {
|
|
151
|
+
await (0, Auth_1.AuthMustBeAdmin)(req, res);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const user = await (0, UsersData_1.UsersDataGet)(context, req.params.id);
|
|
157
|
+
if (!user) {
|
|
158
|
+
return res.status(404).send({ error: "User Not Found" });
|
|
159
|
+
}
|
|
160
|
+
if (req.body.role) {
|
|
161
|
+
user.role = req.body.role === "admin" ? "admin" : "user";
|
|
162
|
+
}
|
|
163
|
+
if (req.body.scopes && Array.isArray(req.body.scopes)) {
|
|
164
|
+
user.scopes = req.body.scopes.filter((s) => User_1.User.ALL_SCOPES.includes(s));
|
|
165
|
+
}
|
|
166
|
+
await (0, UsersData_1.UsersDataUpdateUser)(context, user);
|
|
167
|
+
// If password change requested
|
|
168
|
+
if (req.body.password) {
|
|
169
|
+
await (0, UserPassword_1.UserPasswordSetPassword)(context, user, req.body.password);
|
|
170
|
+
await (0, UsersData_1.UsersDataUpdatePassword)(context, user);
|
|
171
|
+
}
|
|
172
|
+
res.status(201).send({ user: user.toTransportJson() });
|
|
173
|
+
});
|
|
174
|
+
fastify.delete("/:id", async (req, res) => {
|
|
175
|
+
const context = requestSpan(req);
|
|
176
|
+
try {
|
|
177
|
+
await (0, Auth_1.AuthMustBeAdmin)(req, res);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const userSession = await (0, Auth_1.AuthGetUserSession)(req);
|
|
183
|
+
// Cannot delete yourself
|
|
184
|
+
if (userSession.userId === req.params.id) {
|
|
185
|
+
return res.status(400).send({ error: "Cannot Delete Yourself" });
|
|
186
|
+
}
|
|
187
|
+
const user = await (0, UsersData_1.UsersDataGet)(context, req.params.id);
|
|
188
|
+
if (!user) {
|
|
189
|
+
return res.status(404).send({ error: "User Not Found" });
|
|
190
|
+
}
|
|
191
|
+
// Check that at least 1 admin remains
|
|
192
|
+
if (user.role === "admin") {
|
|
193
|
+
const admins = (await (0, UsersData_1.UsersDataList)(context)).filter((u) => u.role === "admin");
|
|
194
|
+
if (admins.length <= 1) {
|
|
195
|
+
return res
|
|
196
|
+
.status(400)
|
|
197
|
+
.send({ error: "At least 1 admin must be defined" });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
await (0, UsersData_1.UsersDataDelete)(context, req.params.id);
|
|
201
|
+
res.status(201).send({});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
exports.UsersRoutes = UsersRoutes;
|
package/index.ts
CHANGED
|
@@ -7,3 +7,9 @@ export * from "./src/PostgresDbUtils";
|
|
|
7
7
|
export * from "./src/Notifications";
|
|
8
8
|
export * from "./src/SystemCommand";
|
|
9
9
|
export * from "./src/Timeout";
|
|
10
|
+
export * from "./src/User";
|
|
11
|
+
export * from "./src/UserSession";
|
|
12
|
+
export * from "./src/Auth";
|
|
13
|
+
export * from "./src/UserPassword";
|
|
14
|
+
export * from "./src/UsersData";
|
|
15
|
+
export * from "./src/UsersRoutes";
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devopsplaybook.io/common-utils",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Shared utility modules for devopsplaybook.io projects (DB, Config, OTel context, notifications, system helpers)",
|
|
3
|
+
"version": "1.4.0-beta.13.4edad2b",
|
|
4
|
+
"description": "Shared utility modules for devopsplaybook.io projects (DB, Config, OTel context, auth/users, notifications, system helpers)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Open Telemetry",
|
|
7
7
|
"OTel",
|
|
8
8
|
"SQLite",
|
|
9
9
|
"Postgres",
|
|
10
10
|
"Config",
|
|
11
|
+
"Auth",
|
|
12
|
+
"JWT",
|
|
13
|
+
"Users",
|
|
11
14
|
"Notifications",
|
|
12
15
|
"Utilities"
|
|
13
16
|
],
|
|
@@ -26,16 +29,21 @@
|
|
|
26
29
|
"@opentelemetry/api": "^1.9.1",
|
|
27
30
|
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
28
31
|
"axios": "^1.19.0",
|
|
32
|
+
"bcrypt": "^6.0.0",
|
|
29
33
|
"better-sqlite3": "^13.0.3",
|
|
34
|
+
"fastify": "^5.12.1",
|
|
30
35
|
"fs-extra": "^11.4.0",
|
|
36
|
+
"jsonwebtoken": "^9.0.3",
|
|
31
37
|
"pg": "^8.23.0",
|
|
32
38
|
"uuid": "^14.0.2"
|
|
33
39
|
},
|
|
34
40
|
"devDependencies": {
|
|
35
41
|
"@eslint/js": "^10.0.1",
|
|
42
|
+
"@types/bcrypt": "^6.0.0",
|
|
36
43
|
"@types/better-sqlite3": "^9.6.0",
|
|
37
44
|
"@types/fs-extra": "^11.0.4",
|
|
38
45
|
"@types/jest": "^30.0.0",
|
|
46
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
39
47
|
"@types/node": "^26.2.0",
|
|
40
48
|
"@types/pg": "^8.23.1",
|
|
41
49
|
"@types/uuid": "^11.0.0",
|
package/src/Auth.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { StandardTracer } from "@devopsplaybook.io/otel-utils";
|
|
2
|
+
import { Span } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
import * as jwt from "jsonwebtoken";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { DbUtilsQuerySQL } from "./DbUtils";
|
|
6
|
+
import { User, UserScope } from "./User";
|
|
7
|
+
import { UserSession } from "./UserSession";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration subset required by the auth module.
|
|
11
|
+
*/
|
|
12
|
+
export interface AuthConfig {
|
|
13
|
+
JWT_KEY: string;
|
|
14
|
+
JWT_VALIDITY_DURATION: number;
|
|
15
|
+
DATABASE_TYPE: "sqlite" | "postgres";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let tracer: StandardTracer;
|
|
19
|
+
let config: AuthConfig;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Injects the OTel tracer instance used by the auth module.
|
|
23
|
+
* Must be called once at startup, before {@link AuthInit}.
|
|
24
|
+
*/
|
|
25
|
+
export function AuthSetOTel(tracerIn: StandardTracer): void {
|
|
26
|
+
tracer = tracerIn;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialise the auth module.
|
|
31
|
+
*
|
|
32
|
+
* Registers the full scope set of the host application and loads the JWT
|
|
33
|
+
* signing key from the `metadata` table. When no key is stored yet, a fresh
|
|
34
|
+
* one is generated and persisted.
|
|
35
|
+
*
|
|
36
|
+
* @param context Parent OTel span.
|
|
37
|
+
* @param configIn Server configuration (JWT_KEY is updated in place).
|
|
38
|
+
* @param allScopes All scopes supported by the host application.
|
|
39
|
+
*/
|
|
40
|
+
export async function AuthInit(
|
|
41
|
+
context: Span,
|
|
42
|
+
configIn: AuthConfig,
|
|
43
|
+
allScopes: UserScope[] = [],
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
config = configIn;
|
|
46
|
+
User.ALL_SCOPES = [...allScopes];
|
|
47
|
+
const span = tracer.startSpan("AuthInit", context);
|
|
48
|
+
const authKeyRaw = await DbUtilsQuerySQL(span, SQL_QUERIES.GET_AUTH_TOKEN);
|
|
49
|
+
if (authKeyRaw.length == 0) {
|
|
50
|
+
configIn.JWT_KEY = uuidv4();
|
|
51
|
+
await DbUtilsQuerySQL(span, SQL_QUERIES.INSERT_AUTH_TOKEN, [
|
|
52
|
+
configIn.JWT_KEY,
|
|
53
|
+
new Date().toISOString(),
|
|
54
|
+
]);
|
|
55
|
+
} else {
|
|
56
|
+
configIn.JWT_KEY = authKeyRaw[0].value;
|
|
57
|
+
}
|
|
58
|
+
span.end();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function AuthGenerateJWT(user: User): Promise<string> {
|
|
62
|
+
return jwt.sign(
|
|
63
|
+
{
|
|
64
|
+
exp: Math.floor(Date.now() / 1000) + config.JWT_VALIDITY_DURATION,
|
|
65
|
+
userId: user.id,
|
|
66
|
+
userName: user.name,
|
|
67
|
+
role: user.role,
|
|
68
|
+
scopes: user.role === "admin" ? User.ALL_SCOPES : user.scopes,
|
|
69
|
+
},
|
|
70
|
+
config.JWT_KEY,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Decode JWT from request, caching result on req._jwtPayload to avoid
|
|
76
|
+
* redundant verification when multiple auth functions are called per request.
|
|
77
|
+
*/
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
function jwtDecodeCached(req: any): any | null {
|
|
80
|
+
if (req._jwtPayload) {
|
|
81
|
+
return req._jwtPayload;
|
|
82
|
+
}
|
|
83
|
+
if (!req.headers.authorization) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const info = jwt.verify(
|
|
88
|
+
req.headers.authorization.split(" ")[1],
|
|
89
|
+
config.JWT_KEY,
|
|
90
|
+
);
|
|
91
|
+
req._jwtPayload = info;
|
|
92
|
+
return info;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function AuthMustBeAuthenticated(
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
req: any,
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
res: any,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
if (!jwtDecodeCached(req)) {
|
|
105
|
+
res.status(403).send({ error: "Access Denied" });
|
|
106
|
+
throw new Error("Access Denied");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
export async function AuthMustBeAdmin(req: any, res: any): Promise<void> {
|
|
112
|
+
const info = jwtDecodeCached(req);
|
|
113
|
+
if (info?.role === "admin") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
res.status(403).send({ error: "Access Denied" });
|
|
117
|
+
throw new Error("Access Denied");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function AuthHasScope(
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
req: any,
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
res: any,
|
|
125
|
+
scope: UserScope,
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
const info = jwtDecodeCached(req);
|
|
128
|
+
if (!info) {
|
|
129
|
+
res.status(403).send({ error: "Access Denied" });
|
|
130
|
+
throw new Error("Access Denied");
|
|
131
|
+
}
|
|
132
|
+
if (info.role === "admin") {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const scopes: UserScope[] = info.scopes || [];
|
|
136
|
+
if (scopes.includes(scope)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
res.status(403).send({ error: "Access Denied" });
|
|
140
|
+
throw new Error("Access Denied");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
|
+
export async function AuthGetUserSession(req: any): Promise<UserSession> {
|
|
145
|
+
const userSession: UserSession = { isAuthenticated: false };
|
|
146
|
+
const info = jwtDecodeCached(req);
|
|
147
|
+
if (info) {
|
|
148
|
+
userSession.userId = info.userId;
|
|
149
|
+
userSession.userName = info.userName;
|
|
150
|
+
userSession.role = info.role;
|
|
151
|
+
userSession.scopes = info.scopes;
|
|
152
|
+
userSession.isAuthenticated = true;
|
|
153
|
+
}
|
|
154
|
+
return userSession;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// SQL
|
|
158
|
+
// Written SQLite-first with quoted identifiers (valid for both backends);
|
|
159
|
+
// the DbUtils facade converts `?` placeholders for Postgres.
|
|
160
|
+
|
|
161
|
+
const SQL_QUERIES = {
|
|
162
|
+
GET_AUTH_TOKEN: "SELECT value FROM metadata WHERE \"type\" = 'auth_token' LIMIT 1",
|
|
163
|
+
INSERT_AUTH_TOKEN:
|
|
164
|
+
'INSERT INTO metadata ("type", "value", "dateCreated") VALUES (\'auth_token\', ?, ?)',
|
|
165
|
+
};
|
package/src/User.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User role. `admin` bypasses scope checks; `user` is restricted
|
|
5
|
+
* to its granted scopes.
|
|
6
|
+
*/
|
|
7
|
+
export type UserRole = "admin" | "user";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scope identifier restricting what a non-admin user can access.
|
|
11
|
+
* Each application defines its own scope set (e.g. `"traces"`, `"metrics"`)
|
|
12
|
+
* and registers it through `AuthInit`.
|
|
13
|
+
*/
|
|
14
|
+
export type UserScope = string;
|
|
15
|
+
|
|
16
|
+
export class User {
|
|
17
|
+
//
|
|
18
|
+
public static DEFAULT_SCOPES: UserScope[] = [];
|
|
19
|
+
/** Full scope set of the host application, registered via `AuthInit`. */
|
|
20
|
+
public static ALL_SCOPES: UserScope[] = [];
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
public static fromJson(json: any): User | null {
|
|
24
|
+
if (!json) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const user = new User();
|
|
28
|
+
if (json.id) {
|
|
29
|
+
user.id = json.id;
|
|
30
|
+
}
|
|
31
|
+
user.id = json.id;
|
|
32
|
+
user.name = json.name;
|
|
33
|
+
user.passwordEncrypted = json.passwordEncrypted;
|
|
34
|
+
user.role = json.role || "user";
|
|
35
|
+
if (json.scopes) {
|
|
36
|
+
try {
|
|
37
|
+
user.scopes =
|
|
38
|
+
typeof json.scopes === "string"
|
|
39
|
+
? JSON.parse(json.scopes)
|
|
40
|
+
: json.scopes;
|
|
41
|
+
} catch {
|
|
42
|
+
user.scopes = [...User.DEFAULT_SCOPES];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return user;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public id: string;
|
|
49
|
+
public name!: string;
|
|
50
|
+
public passwordEncrypted!: string;
|
|
51
|
+
public role: UserRole = "user";
|
|
52
|
+
public scopes: UserScope[] = [...User.DEFAULT_SCOPES];
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
this.id = uuidv4();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
public toJson(): any {
|
|
60
|
+
return {
|
|
61
|
+
id: this.id,
|
|
62
|
+
name: this.name,
|
|
63
|
+
passwordEncrypted: this.passwordEncrypted,
|
|
64
|
+
role: this.role,
|
|
65
|
+
scopes: this.scopes,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
public toTransportJson(): any {
|
|
71
|
+
return {
|
|
72
|
+
id: this.id,
|
|
73
|
+
name: this.name,
|
|
74
|
+
role: this.role,
|
|
75
|
+
scopes: this.scopes,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
jest.mock("uuid", () => ({
|
|
2
|
+
v4: () => "mock-uuid-1234",
|
|
3
|
+
}));
|
|
4
|
+
|
|
5
|
+
import { User } from "./User";
|
|
6
|
+
import {
|
|
7
|
+
UserPasswordCheckPassword,
|
|
8
|
+
UserPasswordSetPassword,
|
|
9
|
+
} from "./UserPassword";
|
|
10
|
+
|
|
11
|
+
test("Password should be successfully verified if it's the same", async () => {
|
|
12
|
+
const password = "testPassword1234";
|
|
13
|
+
const user = new User();
|
|
14
|
+
await UserPasswordSetPassword(null as never, user, password);
|
|
15
|
+
expect(
|
|
16
|
+
await UserPasswordCheckPassword(null as never, user, password),
|
|
17
|
+
).toBeTruthy();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("Password should be faile to be verified if it's not the same", async () => {
|
|
21
|
+
const password = "testPassword1234";
|
|
22
|
+
const passwordWrong = "testPassword12345";
|
|
23
|
+
const user = new User();
|
|
24
|
+
await UserPasswordSetPassword(null as never, user, password);
|
|
25
|
+
expect(
|
|
26
|
+
await UserPasswordCheckPassword(null as never, user, passwordWrong),
|
|
27
|
+
).toBeFalsy();
|
|
28
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Span } from "@opentelemetry/sdk-trace-base";
|
|
2
|
+
import * as bcrypt from "bcrypt";
|
|
3
|
+
import { User } from "./User";
|
|
4
|
+
|
|
5
|
+
export async function UserPasswordSetPassword(
|
|
6
|
+
context: Span | undefined,
|
|
7
|
+
user: User,
|
|
8
|
+
password: string,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const salt = await bcrypt.genSalt(10);
|
|
11
|
+
user.passwordEncrypted = await bcrypt.hash(password, salt);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function UserPasswordCheckPassword(
|
|
15
|
+
context: Span | undefined,
|
|
16
|
+
user: User,
|
|
17
|
+
password: string,
|
|
18
|
+
): Promise<boolean> {
|
|
19
|
+
return await bcrypt.compare(password, user.passwordEncrypted);
|
|
20
|
+
}
|