mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
534e1f1378
## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> Adds a migration that renames `token_name` back to `username`, then adds a new optional column named `token_name` ## Discussion points <!-- Anything about the PR you'd like to discuss before it gets merged? Got any questions or doubts? --> I've added fallbacks for resolving username/tokenname on insert and on making rows from results. But this adds another column renaming, which is worth discussing properly
219 lines
6.5 KiB
TypeScript
219 lines
6.5 KiB
TypeScript
import { EventEmitter } from 'events';
|
|
import metricsHelper from '../util/metrics-helper';
|
|
import { DB_TIME } from '../metric-events';
|
|
import { Logger, LogProvider } from '../logger';
|
|
import NotFoundError from '../error/notfound-error';
|
|
import { IApiTokenStore } from '../types/stores/api-token-store';
|
|
import {
|
|
ApiTokenType,
|
|
IApiToken,
|
|
IApiTokenCreate,
|
|
isAllProjects,
|
|
} from '../types/models/api-token';
|
|
import { ALL_PROJECTS } from '../util/constants';
|
|
import { Db } from './db';
|
|
|
|
const TABLE = 'api_tokens';
|
|
const API_LINK_TABLE = 'api_token_project';
|
|
|
|
const ALL = '*';
|
|
|
|
interface ITokenInsert {
|
|
id: number;
|
|
secret: string;
|
|
username: string;
|
|
type: ApiTokenType;
|
|
expires_at?: Date;
|
|
created_at: Date;
|
|
seen_at?: Date;
|
|
environment: string;
|
|
tokenName?: string;
|
|
}
|
|
|
|
interface ITokenRow extends ITokenInsert {
|
|
project: string;
|
|
}
|
|
|
|
const tokenRowReducer = (acc, tokenRow) => {
|
|
const { project, ...token } = tokenRow;
|
|
if (!acc[tokenRow.secret]) {
|
|
acc[tokenRow.secret] = {
|
|
secret: token.secret,
|
|
tokenName: token.token_name ? token.token_name : token.username,
|
|
type: token.type.toLowerCase(),
|
|
project: ALL,
|
|
projects: [ALL],
|
|
environment: token.environment ? token.environment : ALL,
|
|
expiresAt: token.expires_at,
|
|
createdAt: token.created_at,
|
|
alias: token.alias,
|
|
seenAt: token.seen_at,
|
|
username: token.token_name ? token.token_name : token.username,
|
|
};
|
|
}
|
|
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;
|
|
};
|
|
|
|
const toRow = (newToken: IApiTokenCreate) => ({
|
|
username: newToken.tokenName ?? newToken.username,
|
|
token_name: newToken.tokenName ?? newToken.username,
|
|
secret: newToken.secret,
|
|
type: newToken.type,
|
|
environment:
|
|
newToken.environment === ALL ? undefined : newToken.environment,
|
|
expires_at: newToken.expiresAt,
|
|
alias: newToken.alias || null,
|
|
});
|
|
|
|
const toTokens = (rows: any[]): IApiToken[] => {
|
|
const tokens = rows.reduce(tokenRowReducer, {});
|
|
return Object.values(tokens);
|
|
};
|
|
|
|
export class ApiTokenStore implements IApiTokenStore {
|
|
private logger: Logger;
|
|
|
|
private timer: Function;
|
|
|
|
private db: Db;
|
|
|
|
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
|
|
this.db = db;
|
|
this.logger = getLogger('api-tokens.js');
|
|
this.timer = (action: string) =>
|
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
|
store: 'api-tokens',
|
|
action,
|
|
});
|
|
}
|
|
|
|
count(): Promise<number> {
|
|
return this.db(TABLE)
|
|
.count('*')
|
|
.then((res) => Number(res[0].count));
|
|
}
|
|
|
|
async getAll(): Promise<IApiToken[]> {
|
|
const stopTimer = this.timer('getAll');
|
|
const rows = await this.makeTokenProjectQuery();
|
|
stopTimer();
|
|
return toTokens(rows);
|
|
}
|
|
|
|
async getAllActive(): Promise<IApiToken[]> {
|
|
const stopTimer = this.timer('getAllActive');
|
|
const rows = await this.makeTokenProjectQuery()
|
|
.where('expires_at', 'IS', null)
|
|
.orWhere('expires_at', '>', 'now()');
|
|
stopTimer();
|
|
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',
|
|
'username',
|
|
'token_name',
|
|
'type',
|
|
'expires_at',
|
|
'created_at',
|
|
'alias',
|
|
'seen_at',
|
|
'environment',
|
|
'token_project_link.project',
|
|
);
|
|
}
|
|
|
|
async insert(newToken: IApiTokenCreate): Promise<IApiToken> {
|
|
const response = await this.db.transaction(async (tx) => {
|
|
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,
|
|
username: newToken.tokenName,
|
|
alias: newToken.alias || null,
|
|
project: newToken.projects?.join(',') || '*',
|
|
createdAt: row.created_at,
|
|
};
|
|
});
|
|
return response;
|
|
}
|
|
|
|
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> {
|
|
const row = await this.makeTokenProjectQuery().where(
|
|
'tokens.secret',
|
|
key,
|
|
);
|
|
return toTokens(row)[0];
|
|
}
|
|
|
|
async delete(secret: string): Promise<void> {
|
|
return this.db<ITokenRow>(TABLE).where({ secret }).del();
|
|
}
|
|
|
|
async deleteAll(): Promise<void> {
|
|
return this.db<ITokenRow>(TABLE).del();
|
|
}
|
|
|
|
async setExpiry(secret: string, expiresAt: Date): Promise<IApiToken> {
|
|
const rows = await this.makeTokenProjectQuery()
|
|
.update({ expires_at: expiresAt })
|
|
.where({ secret })
|
|
.returning('*');
|
|
if (rows.length > 0) {
|
|
return toTokens(rows)[0];
|
|
}
|
|
throw new NotFoundError('Could not find api-token.');
|
|
}
|
|
|
|
async markSeenAt(secrets: string[]): Promise<void> {
|
|
const now = new Date();
|
|
try {
|
|
await this.db(TABLE)
|
|
.whereIn('secret', secrets)
|
|
.update({ seen_at: now });
|
|
} catch (err) {
|
|
this.logger.error('Could not update lastSeen, error: ', err);
|
|
}
|
|
}
|
|
}
|