@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/Auth.ts
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { createHash } from "crypto";
|
|
2
|
-
import { StandardTracer } from "@devopsplaybook.io/otel-utils";
|
|
3
|
-
import { Span } from "@opentelemetry/sdk-trace-base";
|
|
4
|
-
import * as jwt from "jsonwebtoken";
|
|
5
|
-
import { v4 as uuidv4 } from "uuid";
|
|
6
|
-
import { DbUtilsExecSQL, DbUtilsQuerySQL } from "../DbUtils";
|
|
7
|
-
import { User, UserScope } from "./User";
|
|
8
|
-
import { UserSession } from "./UserSession";
|
|
9
|
-
import { UsersApiTokensDataGetByTokenHash } from "./UsersApiTokensData";
|
|
10
|
-
import { UsersDataGet } from "./UsersData";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Configuration subset required by the auth module.
|
|
14
|
-
*/
|
|
15
|
-
export interface AuthConfig {
|
|
16
|
-
JWT_KEY: string;
|
|
17
|
-
JWT_VALIDITY_DURATION: number;
|
|
18
|
-
DATABASE_TYPE: "sqlite" | "postgres";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let tracer: StandardTracer;
|
|
22
|
-
let config: AuthConfig;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Injects the OTel tracer instance used by the auth module.
|
|
26
|
-
* Must be called once at startup, before {@link AuthInit}.
|
|
27
|
-
*/
|
|
28
|
-
export function AuthSetOTel(tracerIn: StandardTracer): void {
|
|
29
|
-
tracer = tracerIn;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Initialise the auth module.
|
|
34
|
-
*
|
|
35
|
-
* Registers the full scope set of the host application and loads the JWT
|
|
36
|
-
* signing key from the `metadata` table. When no key is stored yet, a fresh
|
|
37
|
-
* one is generated and persisted.
|
|
38
|
-
*
|
|
39
|
-
* @param context Parent OTel span.
|
|
40
|
-
* @param configIn Server configuration (JWT_KEY is updated in place).
|
|
41
|
-
* @param allScopes All scopes supported by the host application.
|
|
42
|
-
*/
|
|
43
|
-
export async function AuthInit(
|
|
44
|
-
context: Span,
|
|
45
|
-
configIn: AuthConfig,
|
|
46
|
-
allScopes: UserScope[] = [],
|
|
47
|
-
): Promise<void> {
|
|
48
|
-
config = configIn;
|
|
49
|
-
User.ALL_SCOPES = [...allScopes];
|
|
50
|
-
const span = tracer.startSpan("AuthInit", context);
|
|
51
|
-
const authKeyRaw = await DbUtilsQuerySQL(span, SQL_QUERIES.GET_AUTH_TOKEN);
|
|
52
|
-
if (authKeyRaw.length == 0) {
|
|
53
|
-
configIn.JWT_KEY = uuidv4();
|
|
54
|
-
await DbUtilsExecSQL(span, SQL_QUERIES.INSERT_AUTH_TOKEN, [
|
|
55
|
-
configIn.JWT_KEY,
|
|
56
|
-
new Date().toISOString(),
|
|
57
|
-
]);
|
|
58
|
-
} else {
|
|
59
|
-
configIn.JWT_KEY = authKeyRaw[0].value;
|
|
60
|
-
}
|
|
61
|
-
span.end();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function AuthGenerateJWT(user: User): Promise<string> {
|
|
65
|
-
return jwt.sign(
|
|
66
|
-
{
|
|
67
|
-
exp: Math.floor(Date.now() / 1000) + config.JWT_VALIDITY_DURATION,
|
|
68
|
-
userId: user.id,
|
|
69
|
-
userName: user.name,
|
|
70
|
-
role: user.role,
|
|
71
|
-
scopes: user.role === "admin" ? User.ALL_SCOPES : user.scopes,
|
|
72
|
-
},
|
|
73
|
-
config.JWT_KEY,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Decode credentials from request, caching result on req._jwtPayload to
|
|
79
|
-
* avoid redundant resolution when multiple auth functions are called per
|
|
80
|
-
* request.
|
|
81
|
-
*
|
|
82
|
-
* A `Bearer` credential is first verified as a JWT. When JWT verification
|
|
83
|
-
* fails, the credential is resolved as a user API token: the value is
|
|
84
|
-
* SHA-256 hashed and looked up in `users_api_tokens`; on match, a payload
|
|
85
|
-
* mirroring the owning user's live role and scopes is built (valid until
|
|
86
|
-
* the token is revoked).
|
|
87
|
-
*/
|
|
88
|
-
async function jwtDecodeCached(req: any): Promise<any | null> {
|
|
89
|
-
if (req._jwtPayload) {
|
|
90
|
-
return req._jwtPayload;
|
|
91
|
-
}
|
|
92
|
-
if (!req.headers.authorization) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
const info = jwt.verify(
|
|
97
|
-
req.headers.authorization.split(" ")[1],
|
|
98
|
-
config.JWT_KEY,
|
|
99
|
-
);
|
|
100
|
-
req._jwtPayload = info;
|
|
101
|
-
return info;
|
|
102
|
-
} catch {
|
|
103
|
-
const info = await resolveApiToken(req.headers.authorization);
|
|
104
|
-
if (info) {
|
|
105
|
-
req._jwtPayload = info;
|
|
106
|
-
return info;
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Resolve an API token bearer credential to a user-backed payload.
|
|
114
|
-
* Permissions are read from the user record at resolution time, so
|
|
115
|
-
* role/scope changes apply to existing tokens immediately.
|
|
116
|
-
*/
|
|
117
|
-
async function resolveApiToken(authorizationHeader: string): Promise<any | null> {
|
|
118
|
-
const token = authorizationHeader.split(" ")[1];
|
|
119
|
-
if (!token) {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
const span = tracer.startSpan("AuthResolveApiToken");
|
|
123
|
-
const tokenHash = createHash("sha256").update(token).digest("hex");
|
|
124
|
-
const apiToken = await UsersApiTokensDataGetByTokenHash(span, tokenHash);
|
|
125
|
-
let payload: any | null = null;
|
|
126
|
-
if (apiToken) {
|
|
127
|
-
const user = await UsersDataGet(span, apiToken.userId);
|
|
128
|
-
if (user) {
|
|
129
|
-
payload = {
|
|
130
|
-
userId: user.id,
|
|
131
|
-
userName: user.name,
|
|
132
|
-
role: user.role,
|
|
133
|
-
scopes: user.role === "admin" ? [...User.ALL_SCOPES] : user.scopes,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
span.end();
|
|
138
|
-
return payload;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function AuthMustBeAuthenticated(
|
|
142
|
-
req: any,
|
|
143
|
-
res: any,
|
|
144
|
-
): Promise<void> {
|
|
145
|
-
if (!(await jwtDecodeCached(req))) {
|
|
146
|
-
res.status(403).send({ error: "Access Denied" });
|
|
147
|
-
throw new Error("Access Denied");
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function AuthMustBeAdmin(req: any, res: any): Promise<void> {
|
|
152
|
-
const info = await jwtDecodeCached(req);
|
|
153
|
-
if (info?.role === "admin") {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
res.status(403).send({ error: "Access Denied" });
|
|
157
|
-
throw new Error("Access Denied");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export async function AuthHasScope(
|
|
161
|
-
req: any,
|
|
162
|
-
res: any,
|
|
163
|
-
scope: UserScope,
|
|
164
|
-
): Promise<void> {
|
|
165
|
-
const info = await jwtDecodeCached(req);
|
|
166
|
-
if (!info) {
|
|
167
|
-
res.status(403).send({ error: "Access Denied" });
|
|
168
|
-
throw new Error("Access Denied");
|
|
169
|
-
}
|
|
170
|
-
if (info.role === "admin") {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const scopes: UserScope[] = info.scopes || [];
|
|
174
|
-
if (scopes.includes(scope)) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
res.status(403).send({ error: "Access Denied" });
|
|
178
|
-
throw new Error("Access Denied");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export async function AuthGetUserSession(req: any): Promise<UserSession> {
|
|
182
|
-
const userSession: UserSession = { isAuthenticated: false };
|
|
183
|
-
const info = await jwtDecodeCached(req);
|
|
184
|
-
if (info) {
|
|
185
|
-
userSession.userId = info.userId;
|
|
186
|
-
userSession.userName = info.userName;
|
|
187
|
-
userSession.role = info.role;
|
|
188
|
-
userSession.scopes = info.scopes;
|
|
189
|
-
userSession.isAuthenticated = true;
|
|
190
|
-
}
|
|
191
|
-
return userSession;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// SQL
|
|
195
|
-
// Written SQLite-first with quoted identifiers (valid for both backends);
|
|
196
|
-
// the DbUtils facade converts `?` placeholders for Postgres.
|
|
197
|
-
|
|
198
|
-
const SQL_QUERIES = {
|
|
199
|
-
GET_AUTH_TOKEN: "SELECT value FROM metadata WHERE \"type\" = 'auth_token' LIMIT 1",
|
|
200
|
-
INSERT_AUTH_TOKEN:
|
|
201
|
-
'INSERT INTO metadata ("type", "value", "dateCreated") VALUES (\'auth_token\', ?, ?)',
|
|
202
|
-
};
|
package/src/users/User.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
public static fromJson(json: any): User | null {
|
|
23
|
-
if (!json) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
const user = new User();
|
|
27
|
-
if (json.id) {
|
|
28
|
-
user.id = json.id;
|
|
29
|
-
}
|
|
30
|
-
user.id = json.id;
|
|
31
|
-
user.name = json.name;
|
|
32
|
-
user.passwordEncrypted = json.passwordEncrypted;
|
|
33
|
-
user.role = json.role || "user";
|
|
34
|
-
if (json.scopes) {
|
|
35
|
-
try {
|
|
36
|
-
user.scopes =
|
|
37
|
-
typeof json.scopes === "string"
|
|
38
|
-
? JSON.parse(json.scopes)
|
|
39
|
-
: json.scopes;
|
|
40
|
-
} catch {
|
|
41
|
-
user.scopes = [...User.DEFAULT_SCOPES];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return user;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
public id: string;
|
|
48
|
-
public name!: string;
|
|
49
|
-
public passwordEncrypted!: string;
|
|
50
|
-
public role: UserRole = "user";
|
|
51
|
-
public scopes: UserScope[] = [...User.DEFAULT_SCOPES];
|
|
52
|
-
|
|
53
|
-
constructor() {
|
|
54
|
-
this.id = uuidv4();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public toJson(): any {
|
|
58
|
-
return {
|
|
59
|
-
id: this.id,
|
|
60
|
-
name: this.name,
|
|
61
|
-
passwordEncrypted: this.passwordEncrypted,
|
|
62
|
-
role: this.role,
|
|
63
|
-
scopes: this.scopes,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
public toTransportJson(): any {
|
|
68
|
-
return {
|
|
69
|
-
id: this.id,
|
|
70
|
-
name: this.name,
|
|
71
|
-
role: this.role,
|
|
72
|
-
scopes: this.scopes,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* User-scoped API token.
|
|
5
|
-
*
|
|
6
|
-
* Only the SHA-256 hash of the token value is ever stored; the plaintext
|
|
7
|
-
* value is generated at creation time and shown to the user exactly once.
|
|
8
|
-
* A token grants the same permissions as the user it belongs to, resolved
|
|
9
|
-
* live on each request (role/scope changes apply immediately).
|
|
10
|
-
*/
|
|
11
|
-
export class UserApiToken {
|
|
12
|
-
//
|
|
13
|
-
public static fromJson(json: any): UserApiToken | null {
|
|
14
|
-
if (!json) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
const apiToken = new UserApiToken();
|
|
18
|
-
apiToken.id = json.id;
|
|
19
|
-
apiToken.name = json.name;
|
|
20
|
-
apiToken.userId = json.userId;
|
|
21
|
-
apiToken.tokenHash = json.tokenHash;
|
|
22
|
-
apiToken.dateCreated = json.dateCreated;
|
|
23
|
-
return apiToken;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
public id: string;
|
|
27
|
-
public name!: string;
|
|
28
|
-
public userId!: string;
|
|
29
|
-
public tokenHash!: string;
|
|
30
|
-
public dateCreated!: string;
|
|
31
|
-
|
|
32
|
-
constructor() {
|
|
33
|
-
this.id = uuidv4();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
public toJson(): any {
|
|
37
|
-
return {
|
|
38
|
-
id: this.id,
|
|
39
|
-
name: this.name,
|
|
40
|
-
userId: this.userId,
|
|
41
|
-
tokenHash: this.tokenHash,
|
|
42
|
-
dateCreated: this.dateCreated,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Transport representation: never exposes the token hash.
|
|
47
|
-
public toTransportJson(): any {
|
|
48
|
-
return {
|
|
49
|
-
id: this.id,
|
|
50
|
-
name: this.name,
|
|
51
|
-
userId: this.userId,
|
|
52
|
-
dateCreated: this.dateCreated,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|
package/src/users/UserSession.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
jest.mock("uuid", () => ({
|
|
2
|
-
v4: () => "mock-uuid-1234",
|
|
3
|
-
}));
|
|
4
|
-
|
|
5
|
-
jest.mock("../DbUtils", () => ({
|
|
6
|
-
DbUtilsQuerySQL: jest.fn(),
|
|
7
|
-
DbUtilsExecSQL: jest.fn(),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
import { StandardTracer } from "@devopsplaybook.io/otel-utils";
|
|
11
|
-
import { DbUtilsExecSQL, DbUtilsQuerySQL } from "../DbUtils";
|
|
12
|
-
import { UserApiToken } from "./UserApiToken";
|
|
13
|
-
import {
|
|
14
|
-
UsersApiTokensDataAdd,
|
|
15
|
-
UsersApiTokensDataDelete,
|
|
16
|
-
UsersApiTokensDataDeleteByUser,
|
|
17
|
-
UsersApiTokensDataGet,
|
|
18
|
-
UsersApiTokensDataGetByTokenHash,
|
|
19
|
-
UsersApiTokensDataListByUser,
|
|
20
|
-
UsersApiTokensDataSetOTel,
|
|
21
|
-
} from "./UsersApiTokensData";
|
|
22
|
-
|
|
23
|
-
const mockedQuery = DbUtilsQuerySQL as jest.Mock;
|
|
24
|
-
const mockedExec = DbUtilsExecSQL as jest.Mock;
|
|
25
|
-
|
|
26
|
-
const mockTracer = {
|
|
27
|
-
startSpan: () => ({ end: () => undefined }),
|
|
28
|
-
} as unknown as StandardTracer;
|
|
29
|
-
|
|
30
|
-
beforeAll(() => {
|
|
31
|
-
UsersApiTokensDataSetOTel(mockTracer);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
mockedQuery.mockReset();
|
|
36
|
-
mockedExec.mockReset();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("UsersApiTokensData", () => {
|
|
40
|
-
it("should add a token storing only its hash", async () => {
|
|
41
|
-
const apiToken = new UserApiToken();
|
|
42
|
-
apiToken.name = "CI token";
|
|
43
|
-
apiToken.userId = "user-1";
|
|
44
|
-
apiToken.tokenHash = "abc123hash";
|
|
45
|
-
apiToken.dateCreated = "2026-09-14T00:00:00.000Z";
|
|
46
|
-
|
|
47
|
-
await UsersApiTokensDataAdd(undefined, apiToken);
|
|
48
|
-
|
|
49
|
-
expect(mockedExec).toHaveBeenCalledTimes(1);
|
|
50
|
-
const [, sql, params] = mockedExec.mock.calls[0];
|
|
51
|
-
expect(sql).toContain("INSERT INTO users_api_tokens");
|
|
52
|
-
expect(params).toEqual([
|
|
53
|
-
"mock-uuid-1234",
|
|
54
|
-
"CI token",
|
|
55
|
-
"user-1",
|
|
56
|
-
"abc123hash",
|
|
57
|
-
"2026-09-14T00:00:00.000Z",
|
|
58
|
-
]);
|
|
59
|
-
expect(sql).toContain("tokenHash");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("should get a token by hash", async () => {
|
|
63
|
-
mockedQuery.mockResolvedValue([
|
|
64
|
-
{
|
|
65
|
-
id: "token-1",
|
|
66
|
-
name: "CI token",
|
|
67
|
-
userId: "user-1",
|
|
68
|
-
tokenHash: "abc123hash",
|
|
69
|
-
dateCreated: "2026-09-14T00:00:00.000Z",
|
|
70
|
-
},
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
const apiToken = await UsersApiTokensDataGetByTokenHash(
|
|
74
|
-
undefined,
|
|
75
|
-
"abc123hash",
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
expect(mockedQuery).toHaveBeenCalledTimes(1);
|
|
79
|
-
const [, sql, params] = mockedQuery.mock.calls[0];
|
|
80
|
-
expect(sql).toBe('SELECT * FROM users_api_tokens WHERE "tokenHash" = ?');
|
|
81
|
-
expect(params).toEqual(["abc123hash"]);
|
|
82
|
-
expect(apiToken?.id).toBe("token-1");
|
|
83
|
-
expect(apiToken?.userId).toBe("user-1");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("should return null when no token matches the hash", async () => {
|
|
87
|
-
mockedQuery.mockResolvedValue([]);
|
|
88
|
-
|
|
89
|
-
const apiToken = await UsersApiTokensDataGetByTokenHash(
|
|
90
|
-
undefined,
|
|
91
|
-
"unknown-hash",
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
expect(apiToken).toBeNull();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("should get a token by id", async () => {
|
|
98
|
-
mockedQuery.mockResolvedValue([
|
|
99
|
-
{
|
|
100
|
-
id: "token-1",
|
|
101
|
-
name: "CI token",
|
|
102
|
-
userId: "user-1",
|
|
103
|
-
tokenHash: "abc123hash",
|
|
104
|
-
dateCreated: "2026-09-14T00:00:00.000Z",
|
|
105
|
-
},
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
|
-
const apiToken = await UsersApiTokensDataGet(undefined, "token-1");
|
|
109
|
-
|
|
110
|
-
const [, sql, params] = mockedQuery.mock.calls[0];
|
|
111
|
-
expect(sql).toBe('SELECT * FROM users_api_tokens WHERE "id" = ?');
|
|
112
|
-
expect(params).toEqual(["token-1"]);
|
|
113
|
-
expect(apiToken?.name).toBe("CI token");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("should list tokens of a user without exposing the hash in transport json", async () => {
|
|
117
|
-
mockedQuery.mockResolvedValue([
|
|
118
|
-
{
|
|
119
|
-
id: "token-1",
|
|
120
|
-
name: "CI token",
|
|
121
|
-
userId: "user-1",
|
|
122
|
-
tokenHash: "abc123hash",
|
|
123
|
-
dateCreated: "2026-09-14T00:00:00.000Z",
|
|
124
|
-
},
|
|
125
|
-
]);
|
|
126
|
-
|
|
127
|
-
const apiTokens = await UsersApiTokensDataListByUser(undefined, "user-1");
|
|
128
|
-
|
|
129
|
-
const [, sql, params] = mockedQuery.mock.calls[0];
|
|
130
|
-
expect(sql).toBe('SELECT * FROM users_api_tokens WHERE "userId" = ?');
|
|
131
|
-
expect(params).toEqual(["user-1"]);
|
|
132
|
-
expect(apiTokens.length).toBe(1);
|
|
133
|
-
const transport = apiTokens[0].toTransportJson();
|
|
134
|
-
expect(transport).toEqual({
|
|
135
|
-
id: "token-1",
|
|
136
|
-
name: "CI token",
|
|
137
|
-
userId: "user-1",
|
|
138
|
-
dateCreated: "2026-09-14T00:00:00.000Z",
|
|
139
|
-
});
|
|
140
|
-
expect(transport.tokenHash).toBeUndefined();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("should delete a token by id", async () => {
|
|
144
|
-
await UsersApiTokensDataDelete(undefined, "token-1");
|
|
145
|
-
|
|
146
|
-
const [, sql, params] = mockedExec.mock.calls[0];
|
|
147
|
-
expect(sql).toBe('DELETE FROM users_api_tokens WHERE "id" = ?');
|
|
148
|
-
expect(params).toEqual(["token-1"]);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("should delete all tokens of a user", async () => {
|
|
152
|
-
await UsersApiTokensDataDeleteByUser(undefined, "user-1");
|
|
153
|
-
|
|
154
|
-
const [, sql, params] = mockedExec.mock.calls[0];
|
|
155
|
-
expect(sql).toBe('DELETE FROM users_api_tokens WHERE "userId" = ?');
|
|
156
|
-
expect(params).toEqual(["user-1"]);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
@@ -1,125 +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 { UserApiToken } from "./UserApiToken";
|
|
5
|
-
|
|
6
|
-
let tracer: StandardTracer;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Injects the OTel tracer instance used by the API tokens data module.
|
|
10
|
-
* Must be called once at startup, before any `UsersApiTokensData*` function.
|
|
11
|
-
*/
|
|
12
|
-
export function UsersApiTokensDataSetOTel(tracerIn: StandardTracer): void {
|
|
13
|
-
tracer = tracerIn;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function UsersApiTokensDataGet(
|
|
17
|
-
context: Span | undefined,
|
|
18
|
-
id: string,
|
|
19
|
-
): Promise<UserApiToken | null> {
|
|
20
|
-
const span = tracer.startSpan("UsersApiTokensDataGet", context);
|
|
21
|
-
const tokensRaw = await DbUtilsQuerySQL(span, SQL_QUERIES.GET_TOKEN_BY_ID, [
|
|
22
|
-
id,
|
|
23
|
-
]);
|
|
24
|
-
let apiToken: UserApiToken | null = null;
|
|
25
|
-
if (tokensRaw.length > 0) {
|
|
26
|
-
apiToken = fromRaw(tokensRaw[0]);
|
|
27
|
-
}
|
|
28
|
-
span.end();
|
|
29
|
-
return apiToken;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function UsersApiTokensDataGetByTokenHash(
|
|
33
|
-
context: Span | undefined,
|
|
34
|
-
tokenHash: string,
|
|
35
|
-
): Promise<UserApiToken | null> {
|
|
36
|
-
const span = tracer.startSpan("UsersApiTokensDataGetByTokenHash", context);
|
|
37
|
-
const tokensRaw = await DbUtilsQuerySQL(
|
|
38
|
-
span,
|
|
39
|
-
SQL_QUERIES.GET_TOKEN_BY_HASH,
|
|
40
|
-
[tokenHash],
|
|
41
|
-
);
|
|
42
|
-
let apiToken: UserApiToken | null = null;
|
|
43
|
-
if (tokensRaw.length > 0) {
|
|
44
|
-
apiToken = fromRaw(tokensRaw[0]);
|
|
45
|
-
}
|
|
46
|
-
span.end();
|
|
47
|
-
return apiToken;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function UsersApiTokensDataListByUser(
|
|
51
|
-
context: Span | undefined,
|
|
52
|
-
userId: string,
|
|
53
|
-
): Promise<UserApiToken[]> {
|
|
54
|
-
const span = tracer.startSpan("UsersApiTokensDataListByUser", context);
|
|
55
|
-
const tokensRaw = await DbUtilsQuerySQL(
|
|
56
|
-
span,
|
|
57
|
-
SQL_QUERIES.LIST_TOKENS_BY_USER,
|
|
58
|
-
[userId],
|
|
59
|
-
);
|
|
60
|
-
const apiTokens: UserApiToken[] = [];
|
|
61
|
-
for (const tokenRaw of tokensRaw) {
|
|
62
|
-
apiTokens.push(fromRaw(tokenRaw));
|
|
63
|
-
}
|
|
64
|
-
span.end();
|
|
65
|
-
return apiTokens;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function UsersApiTokensDataAdd(
|
|
69
|
-
context: Span | undefined,
|
|
70
|
-
apiToken: UserApiToken,
|
|
71
|
-
): Promise<void> {
|
|
72
|
-
const span = tracer.startSpan("UsersApiTokensDataAdd", context);
|
|
73
|
-
await DbUtilsExecSQL(span, SQL_QUERIES.INSERT_TOKEN, [
|
|
74
|
-
apiToken.id,
|
|
75
|
-
apiToken.name,
|
|
76
|
-
apiToken.userId,
|
|
77
|
-
apiToken.tokenHash,
|
|
78
|
-
apiToken.dateCreated,
|
|
79
|
-
]);
|
|
80
|
-
span.end();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function UsersApiTokensDataDelete(
|
|
84
|
-
context: Span | undefined,
|
|
85
|
-
id: string,
|
|
86
|
-
): Promise<void> {
|
|
87
|
-
const span = tracer.startSpan("UsersApiTokensDataDelete", context);
|
|
88
|
-
await DbUtilsExecSQL(span, SQL_QUERIES.DELETE_TOKEN, [id]);
|
|
89
|
-
span.end();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function UsersApiTokensDataDeleteByUser(
|
|
93
|
-
context: Span | undefined,
|
|
94
|
-
userId: string,
|
|
95
|
-
): Promise<void> {
|
|
96
|
-
const span = tracer.startSpan("UsersApiTokensDataDeleteByUser", context);
|
|
97
|
-
await DbUtilsExecSQL(span, SQL_QUERIES.DELETE_TOKENS_BY_USER, [userId]);
|
|
98
|
-
span.end();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Private Functions
|
|
102
|
-
|
|
103
|
-
function fromRaw(tokenRaw: any): UserApiToken {
|
|
104
|
-
const apiToken = new UserApiToken();
|
|
105
|
-
apiToken.id = tokenRaw.id;
|
|
106
|
-
apiToken.name = tokenRaw.name;
|
|
107
|
-
apiToken.userId = tokenRaw.userId;
|
|
108
|
-
apiToken.tokenHash = tokenRaw.tokenHash;
|
|
109
|
-
apiToken.dateCreated = tokenRaw.dateCreated;
|
|
110
|
-
return apiToken;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// SQL
|
|
114
|
-
// Written SQLite-first with quoted identifiers (valid for both backends);
|
|
115
|
-
// the DbUtils facade converts `?` placeholders for Postgres.
|
|
116
|
-
|
|
117
|
-
const SQL_QUERIES = {
|
|
118
|
-
GET_TOKEN_BY_ID: 'SELECT * FROM users_api_tokens WHERE "id" = ?',
|
|
119
|
-
GET_TOKEN_BY_HASH: 'SELECT * FROM users_api_tokens WHERE "tokenHash" = ?',
|
|
120
|
-
LIST_TOKENS_BY_USER: 'SELECT * FROM users_api_tokens WHERE "userId" = ?',
|
|
121
|
-
INSERT_TOKEN:
|
|
122
|
-
'INSERT INTO users_api_tokens ("id", "name", "userId", "tokenHash", "dateCreated") VALUES (?, ?, ?, ?, ?)',
|
|
123
|
-
DELETE_TOKEN: 'DELETE FROM users_api_tokens WHERE "id" = ?',
|
|
124
|
-
DELETE_TOKENS_BY_USER: 'DELETE FROM users_api_tokens WHERE "userId" = ?',
|
|
125
|
-
};
|