2021-03-29 19:58:11 +02:00
|
|
|
import { EventEmitter } from 'events';
|
2021-05-02 21:11:17 +02:00
|
|
|
import metricsHelper from '../util/metrics-helper';
|
2021-04-29 10:21:29 +02:00
|
|
|
import { DB_TIME } from '../metric-events';
|
2021-03-29 19:58:11 +02:00
|
|
|
import { Logger, LogProvider } from '../logger';
|
|
|
|
import NotFoundError from '../error/notfound-error';
|
2021-09-15 20:28:10 +02:00
|
|
|
import { IApiTokenStore } from '../types/stores/api-token-store';
|
2021-08-12 15:04:37 +02:00
|
|
|
import {
|
|
|
|
ApiTokenType,
|
|
|
|
IApiToken,
|
|
|
|
IApiTokenCreate,
|
2022-04-06 08:11:41 +02:00
|
|
|
isAllProjects,
|
2021-09-15 20:28:10 +02:00
|
|
|
} from '../types/models/api-token';
|
2022-06-09 16:56:13 +02:00
|
|
|
import { ALL_PROJECTS } from '../util/constants';
|
2023-01-30 09:02:44 +01:00
|
|
|
import { Db } from './db';
|
2023-10-06 13:38:32 +02:00
|
|
|
import { inTransaction } from './transaction';
|
2021-03-29 19:58:11 +02:00
|
|
|
|
|
|
|
const TABLE = 'api_tokens';
|
2022-04-06 08:11:41 +02:00
|
|
|
const API_LINK_TABLE = 'api_token_project';
|
2021-03-29 19:58:11 +02:00
|
|
|
|
2021-09-15 20:28:10 +02:00
|
|
|
const ALL = '*';
|
|
|
|
|
2022-04-06 08:11:41 +02:00
|
|
|
interface ITokenInsert {
|
2021-03-29 19:58:11 +02:00
|
|
|
id: number;
|
|
|
|
secret: string;
|
|
|
|
username: string;
|
|
|
|
type: ApiTokenType;
|
|
|
|
expires_at?: Date;
|
|
|
|
created_at: Date;
|
|
|
|
seen_at?: Date;
|
2021-09-15 20:28:10 +02:00
|
|
|
environment: string;
|
2023-05-04 09:56:00 +02:00
|
|
|
tokenName?: string;
|
2022-04-06 08:11:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface ITokenRow extends ITokenInsert {
|
2021-09-15 20:28:10 +02:00
|
|
|
project: string;
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
2022-04-06 08:11:41 +02:00
|
|
|
const tokenRowReducer = (acc, tokenRow) => {
|
|
|
|
const { project, ...token } = tokenRow;
|
|
|
|
if (!acc[tokenRow.secret]) {
|
|
|
|
acc[tokenRow.secret] = {
|
|
|
|
secret: token.secret,
|
2023-05-11 15:33:04 +02:00
|
|
|
tokenName: token.token_name ? token.token_name : token.username,
|
2023-05-09 11:22:21 +02:00
|
|
|
type: token.type.toLowerCase(),
|
2022-04-06 08:11:41 +02:00
|
|
|
project: ALL,
|
|
|
|
projects: [ALL],
|
|
|
|
environment: token.environment ? token.environment : ALL,
|
|
|
|
expiresAt: token.expires_at,
|
|
|
|
createdAt: token.created_at,
|
2022-08-19 10:48:33 +02:00
|
|
|
alias: token.alias,
|
2022-11-30 07:07:13 +01:00
|
|
|
seenAt: token.seen_at,
|
2023-05-11 15:33:04 +02:00
|
|
|
username: token.token_name ? token.token_name : token.username,
|
2022-04-06 08:11:41 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
const currentToken = acc[tokenRow.secret];
|
|
|
|
if (tokenRow.project) {
|
|
|
|
if (isAllProjects(currentToken.projects)) {
|
|
|
|
currentToken.projects = [];
|
|
|
|
}
|
|
|
|
currentToken.projects.push(tokenRow.project);
|
|
|
|
currentToken.project = currentToken.projects.join(',');
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
};
|
|
|
|
|
2021-03-29 19:58:11 +02:00
|
|
|
const toRow = (newToken: IApiTokenCreate) => ({
|
2023-05-11 15:33:04 +02:00
|
|
|
username: newToken.tokenName ?? newToken.username,
|
2023-05-04 09:56:00 +02:00
|
|
|
token_name: newToken.tokenName ?? newToken.username,
|
2021-03-29 19:58:11 +02:00
|
|
|
secret: newToken.secret,
|
|
|
|
type: newToken.type,
|
2021-09-15 20:28:10 +02:00
|
|
|
environment:
|
|
|
|
newToken.environment === ALL ? undefined : newToken.environment,
|
2021-03-29 19:58:11 +02:00
|
|
|
expires_at: newToken.expiresAt,
|
2022-08-19 10:48:33 +02:00
|
|
|
alias: newToken.alias || null,
|
2021-03-29 19:58:11 +02:00
|
|
|
});
|
|
|
|
|
2022-04-06 08:11:41 +02:00
|
|
|
const toTokens = (rows: any[]): IApiToken[] => {
|
|
|
|
const tokens = rows.reduce(tokenRowReducer, {});
|
|
|
|
return Object.values(tokens);
|
|
|
|
};
|
2021-03-29 19:58:11 +02:00
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
export class ApiTokenStore implements IApiTokenStore {
|
2021-03-29 19:58:11 +02:00
|
|
|
private logger: Logger;
|
|
|
|
|
|
|
|
private timer: Function;
|
|
|
|
|
2023-01-30 09:02:44 +01:00
|
|
|
private db: Db;
|
2021-03-29 19:58:11 +02:00
|
|
|
|
2023-01-30 09:02:44 +01:00
|
|
|
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
|
2021-03-29 19:58:11 +02:00
|
|
|
this.db = db;
|
|
|
|
this.logger = getLogger('api-tokens.js');
|
|
|
|
this.timer = (action: string) =>
|
|
|
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
|
|
|
store: 'api-tokens',
|
|
|
|
action,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-29 13:09:30 +01:00
|
|
|
async count(): Promise<number> {
|
2022-01-05 10:00:59 +01:00
|
|
|
return this.db(TABLE)
|
|
|
|
.count('*')
|
|
|
|
.then((res) => Number(res[0].count));
|
|
|
|
}
|
|
|
|
|
2023-11-29 13:09:30 +01:00
|
|
|
async countByType(): Promise<Map<string, number>> {
|
|
|
|
return this.db(TABLE)
|
|
|
|
.select('type')
|
|
|
|
.count('*')
|
|
|
|
.groupBy('type')
|
|
|
|
.then((res) => {
|
|
|
|
const map = new Map<string, number>();
|
|
|
|
res.forEach((row) => {
|
|
|
|
map.set(row.type.toString(), Number(row.count));
|
|
|
|
});
|
|
|
|
return map;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-03-29 19:58:11 +02:00
|
|
|
async getAll(): Promise<IApiToken[]> {
|
|
|
|
const stopTimer = this.timer('getAll');
|
2022-04-06 08:11:41 +02:00
|
|
|
const rows = await this.makeTokenProjectQuery();
|
2021-03-29 19:58:11 +02:00
|
|
|
stopTimer();
|
2022-04-06 08:11:41 +02:00
|
|
|
return toTokens(rows);
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async getAllActive(): Promise<IApiToken[]> {
|
|
|
|
const stopTimer = this.timer('getAllActive');
|
2022-04-06 08:11:41 +02:00
|
|
|
const rows = await this.makeTokenProjectQuery()
|
2021-09-02 10:05:31 +02:00
|
|
|
.where('expires_at', 'IS', null)
|
|
|
|
.orWhere('expires_at', '>', 'now()');
|
2021-03-29 19:58:11 +02:00
|
|
|
stopTimer();
|
2022-04-06 08:11:41 +02:00
|
|
|
return toTokens(rows);
|
|
|
|
}
|
|
|
|
|
|
|
|
private makeTokenProjectQuery() {
|
|
|
|
return this.db<ITokenRow>(`${TABLE} as tokens`)
|
|
|
|
.leftJoin(
|
|
|
|
`${API_LINK_TABLE} as token_project_link`,
|
|
|
|
'tokens.secret',
|
|
|
|
'token_project_link.secret',
|
|
|
|
)
|
|
|
|
.select(
|
|
|
|
'tokens.secret',
|
2023-05-11 15:33:04 +02:00
|
|
|
'username',
|
2023-05-04 09:56:00 +02:00
|
|
|
'token_name',
|
2022-04-06 08:11:41 +02:00
|
|
|
'type',
|
|
|
|
'expires_at',
|
|
|
|
'created_at',
|
2022-08-19 10:48:33 +02:00
|
|
|
'alias',
|
2022-04-06 08:11:41 +02:00
|
|
|
'seen_at',
|
|
|
|
'environment',
|
|
|
|
'token_project_link.project',
|
|
|
|
);
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async insert(newToken: IApiTokenCreate): Promise<IApiToken> {
|
2023-10-06 13:38:32 +02:00
|
|
|
const response = await inTransaction(this.db, async (tx) => {
|
2022-04-06 08:11:41 +02:00
|
|
|
const [row] = await tx<ITokenInsert>(TABLE).insert(
|
|
|
|
toRow(newToken),
|
|
|
|
['created_at'],
|
|
|
|
);
|
|
|
|
|
|
|
|
const updateProjectTasks = (newToken.projects || [])
|
|
|
|
.filter((project) => {
|
|
|
|
return project !== ALL_PROJECTS;
|
|
|
|
})
|
|
|
|
.map((project) => {
|
|
|
|
return tx.raw(
|
|
|
|
`INSERT INTO ${API_LINK_TABLE} VALUES (?, ?)`,
|
|
|
|
[newToken.secret, project],
|
|
|
|
);
|
|
|
|
});
|
|
|
|
await Promise.all(updateProjectTasks);
|
|
|
|
return {
|
|
|
|
...newToken,
|
2023-05-04 09:56:00 +02:00
|
|
|
username: newToken.tokenName,
|
2022-08-19 10:48:33 +02:00
|
|
|
alias: newToken.alias || null,
|
2022-04-06 08:11:41 +02:00
|
|
|
project: newToken.projects?.join(',') || '*',
|
|
|
|
createdAt: row.created_at,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
return response;
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
destroy(): void {}
|
|
|
|
|
|
|
|
async exists(secret: string): Promise<boolean> {
|
|
|
|
const result = await this.db.raw(
|
|
|
|
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE secret = ?) AS present`,
|
|
|
|
[secret],
|
|
|
|
);
|
|
|
|
const { present } = result.rows[0];
|
|
|
|
return present;
|
|
|
|
}
|
|
|
|
|
|
|
|
async get(key: string): Promise<IApiToken> {
|
2022-11-28 10:56:34 +01:00
|
|
|
const row = await this.makeTokenProjectQuery().where(
|
|
|
|
'tokens.secret',
|
|
|
|
key,
|
|
|
|
);
|
2022-04-06 08:11:41 +02:00
|
|
|
return toTokens(row)[0];
|
2021-08-12 15:04:37 +02:00
|
|
|
}
|
|
|
|
|
2021-03-29 19:58:11 +02:00
|
|
|
async delete(secret: string): Promise<void> {
|
2022-04-06 08:11:41 +02:00
|
|
|
return this.db<ITokenRow>(TABLE).where({ secret }).del();
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
2021-05-28 11:10:24 +02:00
|
|
|
async deleteAll(): Promise<void> {
|
2022-04-06 08:11:41 +02:00
|
|
|
return this.db<ITokenRow>(TABLE).del();
|
2021-05-28 11:10:24 +02:00
|
|
|
}
|
|
|
|
|
2021-03-29 19:58:11 +02:00
|
|
|
async setExpiry(secret: string, expiresAt: Date): Promise<IApiToken> {
|
2022-04-06 08:11:41 +02:00
|
|
|
const rows = await this.makeTokenProjectQuery()
|
2021-03-29 19:58:11 +02:00
|
|
|
.update({ expires_at: expiresAt })
|
|
|
|
.where({ secret })
|
|
|
|
.returning('*');
|
|
|
|
if (rows.length > 0) {
|
2022-04-06 08:11:41 +02:00
|
|
|
return toTokens(rows)[0];
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
throw new NotFoundError('Could not find api-token.');
|
|
|
|
}
|
|
|
|
|
|
|
|
async markSeenAt(secrets: string[]): Promise<void> {
|
|
|
|
const now = new Date();
|
|
|
|
try {
|
|
|
|
await this.db(TABLE)
|
2022-11-29 20:46:40 +01:00
|
|
|
.whereIn('secret', secrets)
|
2021-03-29 19:58:11 +02:00
|
|
|
.update({ seen_at: now });
|
|
|
|
} catch (err) {
|
|
|
|
this.logger.error('Could not update lastSeen, error: ', err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|