@devopsplaybook.io/common-utils 1.10.1-beta.23.b1906d7 → 1.11.0-beta.24.d8f9aef
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/README.md +73 -27
- package/dist/src/ConfigBase.d.ts +31 -0
- package/dist/src/ConfigBase.js +58 -16
- package/dist/src/DbUtils.d.ts +20 -3
- package/dist/src/DbUtils.js +93 -2
- package/dist/src/DbUtilsNoTelemetry.d.ts +4 -1
- package/dist/src/DbUtilsNoTelemetry.js +62 -6
- package/dist/src/PostgresDbUtils.d.ts +41 -10
- package/dist/src/PostgresDbUtils.js +357 -304
- package/dist/src/SqlDbUtils.d.ts +12 -4
- package/dist/src/SqlDbUtils.js +76 -30
- package/dist/src/users/Auth.d.ts +11 -1
- package/dist/src/users/Auth.js +160 -44
- package/dist/src/users/User.d.ts +10 -0
- package/dist/src/users/User.js +30 -10
- package/dist/src/users/UserApiToken.d.ts +4 -0
- package/dist/src/users/UserApiToken.js +11 -0
- package/dist/src/users/UsersApiTokensData.d.ts +12 -0
- package/dist/src/users/UsersApiTokensData.js +130 -33
- package/dist/src/users/UsersData.d.ts +20 -1
- package/dist/src/users/UsersData.js +150 -52
- package/dist/src/users/UsersRoutes.js +178 -61
- package/dist/src/users/index.d.ts +8 -0
- package/dist/src/users/index.js +24 -0
- package/package.json +58 -1
- package/.github/workflows/main-build.yml +0 -18
- package/.github/workflows/pr-check.yml +0 -27
- package/.github/workflows/reusable-merge-build.yml +0 -197
- package/.github/workflows/reusable-npm-merge.yml +0 -135
- package/.github/workflows/reusable-npm-pr.yml +0 -183
- package/.github/workflows/reusable-npm-upgrade.yml +0 -92
- package/.github/workflows/reusable-pr-verify.yml +0 -181
- package/AGENTS.md +0 -105
- package/index.ts +0 -18
- package/jest.config.js +0 -17
- package/prettierrc.json +0 -5
- package/src/ConfigBase.spec.ts +0 -108
- package/src/ConfigBase.ts +0 -297
- package/src/DbUtils.spec.ts +0 -23
- package/src/DbUtils.ts +0 -116
- package/src/DbUtilsNoTelemetry.spec.ts +0 -168
- package/src/DbUtilsNoTelemetry.ts +0 -117
- package/src/LLM.spec.ts +0 -303
- package/src/LLM.ts +0 -204
- package/src/Notifications.spec.ts +0 -265
- package/src/Notifications.ts +0 -201
- package/src/OTelContext.spec.ts +0 -58
- package/src/OTelContext.ts +0 -63
- package/src/PostgresDbUtils.spec.ts +0 -153
- package/src/PostgresDbUtils.ts +0 -666
- package/src/SqlDbUtils.spec.ts +0 -108
- package/src/SqlDbUtils.ts +0 -152
- package/src/SystemCommand.spec.ts +0 -18
- package/src/SystemCommand.ts +0 -23
- package/src/Timeout.spec.ts +0 -18
- package/src/Timeout.ts +0 -12
- package/src/users/Auth.spec.ts +0 -268
- package/src/users/Auth.ts +0 -202
- package/src/users/User.ts +0 -75
- package/src/users/UserApiToken.ts +0 -55
- package/src/users/UserPassword.spec.ts +0 -28
- package/src/users/UserPassword.ts +0 -20
- package/src/users/UserSession.ts +0 -9
- package/src/users/UsersApiTokensData.spec.ts +0 -158
- package/src/users/UsersApiTokensData.ts +0 -125
- package/src/users/UsersData.ts +0 -141
- package/src/users/UsersRoutes.ts +0 -374
- package/tsconfig.json +0 -15
- package/tsconfig.spec.json +0 -8
package/src/users/UsersData.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { StandardTracer } from "@devopsplaybook.io/otel-utils";
|
|
2
|
-
import { Span } from "@opentelemetry/sdk-trace-base";
|
|
3
|
-
import { DbUtilsExecSQL, DbUtilsQuerySQL } from "../DbUtils";
|
|
4
|
-
import { User } from "./User";
|
|
5
|
-
|
|
6
|
-
let tracer: StandardTracer;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Injects the OTel tracer instance used by the users data module.
|
|
10
|
-
* Must be called once at startup, before any `UsersData*` function.
|
|
11
|
-
*/
|
|
12
|
-
export function UsersDataSetOTel(tracerIn: StandardTracer): void {
|
|
13
|
-
tracer = tracerIn;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function UsersDataGet(
|
|
17
|
-
context: Span | undefined,
|
|
18
|
-
id: string,
|
|
19
|
-
): Promise<User | null> {
|
|
20
|
-
const span = tracer.startSpan("UsersDataGet", context);
|
|
21
|
-
const usersRaw = await DbUtilsQuerySQL(span, SQL_QUERIES.GET_USER_BY_ID, [
|
|
22
|
-
id,
|
|
23
|
-
]);
|
|
24
|
-
let user: User | null = null;
|
|
25
|
-
if (usersRaw.length > 0) {
|
|
26
|
-
user = fromRaw(usersRaw[0]);
|
|
27
|
-
}
|
|
28
|
-
span.end();
|
|
29
|
-
return user;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function UsersDataGetByName(
|
|
33
|
-
context: Span | undefined,
|
|
34
|
-
name: string,
|
|
35
|
-
): Promise<User | null> {
|
|
36
|
-
const span = tracer.startSpan("UsersDataGetByName", context);
|
|
37
|
-
const usersRaw = await DbUtilsQuerySQL(span, SQL_QUERIES.GET_USER_BY_NAME, [
|
|
38
|
-
name,
|
|
39
|
-
]);
|
|
40
|
-
let user: User | null = null;
|
|
41
|
-
if (usersRaw.length > 0) {
|
|
42
|
-
user = fromRaw(usersRaw[0]);
|
|
43
|
-
}
|
|
44
|
-
span.end();
|
|
45
|
-
return user;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function UsersDataList(
|
|
49
|
-
context: Span | undefined,
|
|
50
|
-
): Promise<User[]> {
|
|
51
|
-
const span = tracer.startSpan("UsersDataList", context);
|
|
52
|
-
const usersRaw = await DbUtilsQuerySQL(span, SQL_QUERIES.LIST_USERS);
|
|
53
|
-
const users: User[] = [];
|
|
54
|
-
for (const userRaw of usersRaw) {
|
|
55
|
-
users.push(fromRaw(userRaw));
|
|
56
|
-
}
|
|
57
|
-
span.end();
|
|
58
|
-
return users;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function UsersDataAdd(
|
|
62
|
-
context: Span | undefined,
|
|
63
|
-
user: User,
|
|
64
|
-
): Promise<void> {
|
|
65
|
-
const span = tracer.startSpan("UsersDataAdd", context);
|
|
66
|
-
await DbUtilsExecSQL(span, SQL_QUERIES.INSERT_USER, [
|
|
67
|
-
user.id,
|
|
68
|
-
user.name,
|
|
69
|
-
user.passwordEncrypted,
|
|
70
|
-
user.role,
|
|
71
|
-
JSON.stringify(user.scopes),
|
|
72
|
-
]);
|
|
73
|
-
span.end();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function UsersDataUpdatePassword(
|
|
77
|
-
context: Span | undefined,
|
|
78
|
-
user: User,
|
|
79
|
-
): Promise<void> {
|
|
80
|
-
const span = tracer.startSpan("UsersDataUpdatePassword", context);
|
|
81
|
-
await DbUtilsExecSQL(span, SQL_QUERIES.UPDATE_PASSWORD, [
|
|
82
|
-
user.passwordEncrypted,
|
|
83
|
-
user.id,
|
|
84
|
-
]);
|
|
85
|
-
span.end();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export async function UsersDataUpdateUser(
|
|
89
|
-
context: Span | undefined,
|
|
90
|
-
user: User,
|
|
91
|
-
): Promise<void> {
|
|
92
|
-
const span = tracer.startSpan("UsersDataUpdateUser", context);
|
|
93
|
-
await DbUtilsExecSQL(span, SQL_QUERIES.UPDATE_USER, [
|
|
94
|
-
user.role,
|
|
95
|
-
JSON.stringify(user.scopes),
|
|
96
|
-
user.id,
|
|
97
|
-
]);
|
|
98
|
-
span.end();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function UsersDataDelete(
|
|
102
|
-
context: Span | undefined,
|
|
103
|
-
id: string,
|
|
104
|
-
): Promise<void> {
|
|
105
|
-
const span = tracer.startSpan("UsersDataDelete", context);
|
|
106
|
-
await DbUtilsExecSQL(span, SQL_QUERIES.DELETE_USER, [id]);
|
|
107
|
-
span.end();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Private Functions
|
|
111
|
-
|
|
112
|
-
function fromRaw(userRaw: any): User {
|
|
113
|
-
const user = new User();
|
|
114
|
-
user.id = userRaw.id;
|
|
115
|
-
user.name = userRaw.name;
|
|
116
|
-
user.passwordEncrypted = userRaw.passwordEncrypted;
|
|
117
|
-
user.role = userRaw.role || "user";
|
|
118
|
-
if (userRaw.scopes) {
|
|
119
|
-
try {
|
|
120
|
-
user.scopes = JSON.parse(userRaw.scopes);
|
|
121
|
-
} catch {
|
|
122
|
-
user.scopes = [...User.DEFAULT_SCOPES];
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return user;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// SQL
|
|
129
|
-
// Written SQLite-first with quoted identifiers (valid for both backends);
|
|
130
|
-
// the DbUtils facade converts `?` placeholders for Postgres.
|
|
131
|
-
|
|
132
|
-
const SQL_QUERIES = {
|
|
133
|
-
GET_USER_BY_ID: 'SELECT * FROM users WHERE "id" = ?',
|
|
134
|
-
GET_USER_BY_NAME: 'SELECT * FROM users WHERE "name" = ?',
|
|
135
|
-
LIST_USERS: "SELECT * FROM users",
|
|
136
|
-
INSERT_USER:
|
|
137
|
-
'INSERT INTO users ("id", "name", "passwordEncrypted", "role", "scopes") VALUES (?, ?, ?, ?, ?)',
|
|
138
|
-
UPDATE_USER: 'UPDATE users SET "role" = ?, "scopes" = ? WHERE "id" = ?',
|
|
139
|
-
UPDATE_PASSWORD: 'UPDATE users SET "passwordEncrypted" = ? WHERE "id" = ?',
|
|
140
|
-
DELETE_USER: 'DELETE FROM users WHERE "id" = ?',
|
|
141
|
-
};
|
package/src/users/UsersRoutes.ts
DELETED
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
import { createHash } from "crypto";
|
|
2
|
-
import { Span } from "@opentelemetry/sdk-trace-base";
|
|
3
|
-
import { FastifyInstance, RequestGenericInterface } from "fastify";
|
|
4
|
-
import { v4 as uuidv4 } from "uuid";
|
|
5
|
-
import { AuthGenerateJWT, AuthGetUserSession, AuthMustBeAdmin } from "./Auth";
|
|
6
|
-
import { User } from "./User";
|
|
7
|
-
import { UserApiToken } from "./UserApiToken";
|
|
8
|
-
import {
|
|
9
|
-
UserPasswordCheckPassword,
|
|
10
|
-
UserPasswordSetPassword,
|
|
11
|
-
} from "./UserPassword";
|
|
12
|
-
import {
|
|
13
|
-
UsersDataAdd,
|
|
14
|
-
UsersDataDelete,
|
|
15
|
-
UsersDataGet,
|
|
16
|
-
UsersDataGetByName,
|
|
17
|
-
UsersDataList,
|
|
18
|
-
UsersDataUpdatePassword,
|
|
19
|
-
UsersDataUpdateUser,
|
|
20
|
-
} from "./UsersData";
|
|
21
|
-
import {
|
|
22
|
-
UsersApiTokensDataAdd,
|
|
23
|
-
UsersApiTokensDataDelete,
|
|
24
|
-
UsersApiTokensDataDeleteByUser,
|
|
25
|
-
UsersApiTokensDataGet,
|
|
26
|
-
UsersApiTokensDataListByUser,
|
|
27
|
-
} from "./UsersApiTokensData";
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Retrieves the OTel span attached to the request by the
|
|
31
|
-
* `@devopsplaybook.io/otel-utils-fastify` hooks.
|
|
32
|
-
*/
|
|
33
|
-
function requestSpan(req: any): Span | undefined {
|
|
34
|
-
return req?.tracerSpanApi;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Standard user management routes shared across applications:
|
|
39
|
-
* initialization status, login (session), user CRUD and password changes.
|
|
40
|
-
*
|
|
41
|
-
* Register on a fastify instance:
|
|
42
|
-
* ```ts
|
|
43
|
-
* fastify.register(new UsersRoutes().getRoutes, { prefix: "/api/users" });
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
export class UsersRoutes {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
public async getRoutes(fastify: FastifyInstance): Promise<void> {
|
|
50
|
-
//
|
|
51
|
-
fastify.get("/status/initialization", async (req, res) => {
|
|
52
|
-
if ((await UsersDataList(requestSpan(req))).length === 0) {
|
|
53
|
-
res.status(201).send({ initialized: false });
|
|
54
|
-
} else {
|
|
55
|
-
res.status(201).send({ initialized: true });
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// ==================== SESSION (Login) ====================
|
|
60
|
-
|
|
61
|
-
interface PostSession extends RequestGenericInterface {
|
|
62
|
-
Body: {
|
|
63
|
-
name: string;
|
|
64
|
-
password: string;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
fastify.post<PostSession>("/session", async (req, res) => {
|
|
68
|
-
let user: User | null;
|
|
69
|
-
// From token
|
|
70
|
-
const userSession = await AuthGetUserSession(req);
|
|
71
|
-
if (userSession.isAuthenticated) {
|
|
72
|
-
// isAuthenticated implies userId is set
|
|
73
|
-
user = await UsersDataGet(
|
|
74
|
-
requestSpan(req),
|
|
75
|
-
userSession.userId as string,
|
|
76
|
-
);
|
|
77
|
-
if (!user) {
|
|
78
|
-
return res.status(403).send({ error: "Authentication Failed" });
|
|
79
|
-
}
|
|
80
|
-
return res.status(201).send({
|
|
81
|
-
success: true,
|
|
82
|
-
token: await AuthGenerateJWT(user),
|
|
83
|
-
user: user.toTransportJson(),
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// From User/Pass
|
|
88
|
-
if (!req.body.name) {
|
|
89
|
-
return res.status(400).send({ error: "Missing: Name" });
|
|
90
|
-
}
|
|
91
|
-
if (!req.body.password) {
|
|
92
|
-
return res.status(400).send({ error: "Missing: Password" });
|
|
93
|
-
}
|
|
94
|
-
user = await UsersDataGetByName(requestSpan(req), req.body.name);
|
|
95
|
-
if (!user) {
|
|
96
|
-
return res.status(403).send({ error: "Authentication Failed" });
|
|
97
|
-
} else if (
|
|
98
|
-
await UserPasswordCheckPassword(
|
|
99
|
-
requestSpan(req),
|
|
100
|
-
user,
|
|
101
|
-
req.body.password,
|
|
102
|
-
)
|
|
103
|
-
) {
|
|
104
|
-
return res.status(201).send({
|
|
105
|
-
success: true,
|
|
106
|
-
token: await AuthGenerateJWT(user),
|
|
107
|
-
user: user.toTransportJson(),
|
|
108
|
-
});
|
|
109
|
-
} else {
|
|
110
|
-
return res.status(403).send({ error: "Authentication Failed" });
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// ==================== LIST USERS (Admin only) ====================
|
|
115
|
-
|
|
116
|
-
fastify.get("/", async (req, res) => {
|
|
117
|
-
try {
|
|
118
|
-
await AuthMustBeAdmin(req, res);
|
|
119
|
-
} catch {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const users = await UsersDataList(requestSpan(req));
|
|
123
|
-
return res.status(200).send(users.map((u) => u.toTransportJson()));
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// ==================== CREATE USER ====================
|
|
127
|
-
|
|
128
|
-
interface PostUser extends RequestGenericInterface {
|
|
129
|
-
Body: {
|
|
130
|
-
name: string;
|
|
131
|
-
password: string;
|
|
132
|
-
role?: string;
|
|
133
|
-
scopes?: string[];
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
fastify.post<PostUser>("/", async (req, res) => {
|
|
137
|
-
const context = requestSpan(req);
|
|
138
|
-
let isInitialized = true;
|
|
139
|
-
if ((await UsersDataList(context)).length === 0) {
|
|
140
|
-
isInitialized = false;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// If initialized, only admin can create users
|
|
144
|
-
if (isInitialized) {
|
|
145
|
-
try {
|
|
146
|
-
await AuthMustBeAdmin(req, res);
|
|
147
|
-
} catch {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!req.body.name) {
|
|
153
|
-
return res.status(400).send({ error: "Missing: Name" });
|
|
154
|
-
}
|
|
155
|
-
if (!req.body.password) {
|
|
156
|
-
return res.status(400).send({ error: "Missing: Password" });
|
|
157
|
-
}
|
|
158
|
-
if (await UsersDataGetByName(context, req.body.name)) {
|
|
159
|
-
return res.status(400).send({ error: "Username Already Exists" });
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const newUser = new User();
|
|
163
|
-
newUser.name = req.body.name;
|
|
164
|
-
// First user is always admin
|
|
165
|
-
if (isInitialized) {
|
|
166
|
-
newUser.role = req.body.role === "admin" ? "admin" : "user";
|
|
167
|
-
if (req.body.scopes && Array.isArray(req.body.scopes)) {
|
|
168
|
-
newUser.scopes = req.body.scopes.filter((s) =>
|
|
169
|
-
User.ALL_SCOPES.includes(s),
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
newUser.role = "admin";
|
|
174
|
-
newUser.scopes = [...User.ALL_SCOPES];
|
|
175
|
-
}
|
|
176
|
-
await UserPasswordSetPassword(context, newUser, req.body.password);
|
|
177
|
-
await UsersDataAdd(context, newUser);
|
|
178
|
-
res.status(201).send({ user: newUser.toTransportJson() });
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// ==================== ADMIN: CHANGE OWN PASSWORD ====================
|
|
182
|
-
|
|
183
|
-
interface PutOwnPassword extends RequestGenericInterface {
|
|
184
|
-
Body: {
|
|
185
|
-
password: string;
|
|
186
|
-
passwordOld: string;
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
fastify.put<PutOwnPassword>("/password", async (req, res) => {
|
|
190
|
-
const context = requestSpan(req);
|
|
191
|
-
const userSession = await AuthGetUserSession(req);
|
|
192
|
-
if (!userSession.isAuthenticated) {
|
|
193
|
-
return res.status(403).send({ error: "Access Denied" });
|
|
194
|
-
}
|
|
195
|
-
// isAuthenticated implies userId is set
|
|
196
|
-
const user = await UsersDataGet(context, userSession.userId as string);
|
|
197
|
-
if (!user) {
|
|
198
|
-
return res.status(403).send({ error: "Access Denied" });
|
|
199
|
-
}
|
|
200
|
-
if (!req.body.password) {
|
|
201
|
-
return res.status(400).send({ error: "Missing: Password" });
|
|
202
|
-
}
|
|
203
|
-
if (
|
|
204
|
-
!(await UserPasswordCheckPassword(context, user, req.body.passwordOld))
|
|
205
|
-
) {
|
|
206
|
-
return res.status(403).send({ error: "Old Password Wrong" });
|
|
207
|
-
}
|
|
208
|
-
await UserPasswordSetPassword(context, user, req.body.password);
|
|
209
|
-
await UsersDataUpdatePassword(context, user);
|
|
210
|
-
res.status(201).send({});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// ==================== ADMIN: UPDATE USER (role, scopes, password) ====================
|
|
214
|
-
|
|
215
|
-
interface PutUser extends RequestGenericInterface {
|
|
216
|
-
Params: {
|
|
217
|
-
id: string;
|
|
218
|
-
};
|
|
219
|
-
Body: {
|
|
220
|
-
role?: string;
|
|
221
|
-
scopes?: string[];
|
|
222
|
-
password?: string;
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
fastify.put<PutUser>("/:id", async (req, res) => {
|
|
226
|
-
const context = requestSpan(req);
|
|
227
|
-
try {
|
|
228
|
-
await AuthMustBeAdmin(req, res);
|
|
229
|
-
} catch {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const user = await UsersDataGet(context, req.params.id);
|
|
234
|
-
if (!user) {
|
|
235
|
-
return res.status(404).send({ error: "User Not Found" });
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (req.body.role) {
|
|
239
|
-
user.role = req.body.role === "admin" ? "admin" : "user";
|
|
240
|
-
}
|
|
241
|
-
if (req.body.scopes && Array.isArray(req.body.scopes)) {
|
|
242
|
-
user.scopes = req.body.scopes.filter((s) =>
|
|
243
|
-
User.ALL_SCOPES.includes(s),
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
await UsersDataUpdateUser(context, user);
|
|
248
|
-
|
|
249
|
-
// If password change requested
|
|
250
|
-
if (req.body.password) {
|
|
251
|
-
await UserPasswordSetPassword(context, user, req.body.password);
|
|
252
|
-
await UsersDataUpdatePassword(context, user);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
res.status(201).send({ user: user.toTransportJson() });
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// ==================== ADMIN: DELETE USER ====================
|
|
259
|
-
|
|
260
|
-
interface DeleteUser extends RequestGenericInterface {
|
|
261
|
-
Params: {
|
|
262
|
-
id: string;
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
fastify.delete<DeleteUser>("/:id", async (req, res) => {
|
|
266
|
-
const context = requestSpan(req);
|
|
267
|
-
try {
|
|
268
|
-
await AuthMustBeAdmin(req, res);
|
|
269
|
-
} catch {
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const userSession = await AuthGetUserSession(req);
|
|
274
|
-
|
|
275
|
-
// Cannot delete yourself
|
|
276
|
-
if (userSession.userId === req.params.id) {
|
|
277
|
-
return res.status(400).send({ error: "Cannot Delete Yourself" });
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const user = await UsersDataGet(context, req.params.id);
|
|
281
|
-
if (!user) {
|
|
282
|
-
return res.status(404).send({ error: "User Not Found" });
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Check that at least 1 admin remains
|
|
286
|
-
if (user.role === "admin") {
|
|
287
|
-
const admins = (await UsersDataList(context)).filter(
|
|
288
|
-
(u) => u.role === "admin",
|
|
289
|
-
);
|
|
290
|
-
if (admins.length <= 1) {
|
|
291
|
-
return res
|
|
292
|
-
.status(400)
|
|
293
|
-
.send({ error: "At least 1 admin must be defined" });
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
await UsersDataDelete(context, req.params.id);
|
|
298
|
-
await UsersApiTokensDataDeleteByUser(context, req.params.id);
|
|
299
|
-
res.status(201).send({});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// ==================== API TOKENS (self-service) ====================
|
|
303
|
-
|
|
304
|
-
interface PostApiToken extends RequestGenericInterface {
|
|
305
|
-
Body: {
|
|
306
|
-
name: string;
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
fastify.post<PostApiToken>("/tokens", async (req, res) => {
|
|
310
|
-
const context = requestSpan(req);
|
|
311
|
-
const userSession = await AuthGetUserSession(req);
|
|
312
|
-
if (!userSession.isAuthenticated) {
|
|
313
|
-
return res.status(403).send({ error: "Access Denied" });
|
|
314
|
-
}
|
|
315
|
-
if (!req.body || !req.body.name) {
|
|
316
|
-
return res.status(400).send({ error: "Missing: Name" });
|
|
317
|
-
}
|
|
318
|
-
const apiToken = new UserApiToken();
|
|
319
|
-
apiToken.name = req.body.name;
|
|
320
|
-
// isAuthenticated implies userId is set
|
|
321
|
-
apiToken.userId = userSession.userId as string;
|
|
322
|
-
apiToken.dateCreated = new Date().toISOString();
|
|
323
|
-
const token = uuidv4();
|
|
324
|
-
apiToken.tokenHash = createHash("sha256").update(token).digest("hex");
|
|
325
|
-
await UsersApiTokensDataAdd(context, apiToken);
|
|
326
|
-
// The plaintext token is returned once; only its hash is stored.
|
|
327
|
-
return res.status(201).send({
|
|
328
|
-
token,
|
|
329
|
-
...apiToken.toTransportJson(),
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
fastify.get("/tokens", async (req, res) => {
|
|
334
|
-
const context = requestSpan(req);
|
|
335
|
-
const userSession = await AuthGetUserSession(req);
|
|
336
|
-
if (!userSession.isAuthenticated) {
|
|
337
|
-
return res.status(403).send({ error: "Access Denied" });
|
|
338
|
-
}
|
|
339
|
-
const apiTokens = await UsersApiTokensDataListByUser(
|
|
340
|
-
context,
|
|
341
|
-
userSession.userId as string,
|
|
342
|
-
);
|
|
343
|
-
return res
|
|
344
|
-
.status(200)
|
|
345
|
-
.send(apiTokens.map((t) => t.toTransportJson()));
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
interface DeleteApiToken extends RequestGenericInterface {
|
|
349
|
-
Params: {
|
|
350
|
-
id: string;
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
fastify.delete<DeleteApiToken>("/tokens/:id", async (req, res) => {
|
|
354
|
-
const context = requestSpan(req);
|
|
355
|
-
const userSession = await AuthGetUserSession(req);
|
|
356
|
-
if (!userSession.isAuthenticated) {
|
|
357
|
-
return res.status(403).send({ error: "Access Denied" });
|
|
358
|
-
}
|
|
359
|
-
const apiToken = await UsersApiTokensDataGet(context, req.params.id);
|
|
360
|
-
if (!apiToken) {
|
|
361
|
-
return res.status(404).send({ error: "API Token Not Found" });
|
|
362
|
-
}
|
|
363
|
-
// Owner can always revoke; admins can revoke any token
|
|
364
|
-
if (
|
|
365
|
-
apiToken.userId !== userSession.userId &&
|
|
366
|
-
userSession.role !== "admin"
|
|
367
|
-
) {
|
|
368
|
-
return res.status(403).send({ error: "Access Denied" });
|
|
369
|
-
}
|
|
370
|
-
await UsersApiTokensDataDelete(context, apiToken.id);
|
|
371
|
-
return res.status(201).send({});
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2019",
|
|
4
|
-
"lib": ["ES2019", "ES2022.Error"],
|
|
5
|
-
"module": "commonjs",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["index.ts", "src/**/*"],
|
|
14
|
-
"exclude": ["**/*.spec.ts"]
|
|
15
|
-
}
|