packages/fxa-auth-server/scripts/delete-inactive-accounts/lib.ts (148 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
Account,
Email,
SecurityEvent,
SessionToken as SessionTokenOrm,
} from 'fxa-shared/db/models/auth';
import { SessionToken } from 'fxa-shared/connected-services/models/SessionToken';
import { EVENT_NAMES } from 'fxa-shared/db/models/auth/security-event';
export const setDateToUTC = (someDate: number) => {
const utcDate = new Date(someDate);
utcDate.setUTCHours(0, 0, 0, 0);
return utcDate;
};
export const emailUidsQuery = (activeByDateTimestamp) =>
Email.query()
.distinct('uid')
.where('verifiedAt', '>=', activeByDateTimestamp)
.as('emailUids');
export const sessionTokenUidsQuery = (activeByDateTimestamp) =>
SessionTokenOrm.query()
.distinct('uid')
.where('lastAccessTime', '>=', activeByDateTimestamp)
.as('sessionTokenUids');
export const securityEventUidsQuery = (activeByDateTimestamp) =>
SecurityEvent.query()
.distinct('uid')
.where('createdAt', '>=', activeByDateTimestamp)
.whereIn('nameId', [
EVENT_NAMES['account.login'],
EVENT_NAMES['account.password_reset_success'],
EVENT_NAMES['account.password_changed'],
EVENT_NAMES['session.destroy'],
])
.as('securityEventUids');
export const accountWhereAndOrderByQueryBuilder = (
startDateTimestamp,
endDateTimestamp,
activeByDateTimestamp
) => {
const emailUids = emailUidsQuery(activeByDateTimestamp);
const sessionTokenUids = sessionTokenUidsQuery(activeByDateTimestamp);
const securityEventUids = securityEventUidsQuery(activeByDateTimestamp);
return Account.query()
.leftJoin(emailUids, 'emailUids.uid', 'accounts.uid')
.leftJoin(sessionTokenUids, 'sessionTokenUids.uid', 'accounts.uid')
.leftJoin(securityEventUids, 'securityEventUids.uid', 'accounts.uid')
.where('accounts.emailVerified', 1)
.where('accounts.createdAt', '>=', startDateTimestamp)
.where('accounts.createdAt', '<', endDateTimestamp)
.where((builder) => {
builder
.whereNull('emailUids.uid')
.whereNull('sessionTokenUids.uid')
.whereNull('securityEventUids.uid');
})
.orderBy('accounts.createdAt', 'asc')
.orderBy('accounts.uid', 'asc');
};
export type GetTokensFn<T> = (uid: string) => Promise<T[]>;
// this includes the agumented last access time from redis
export const hasActiveSessionToken = async (
tokensFn: GetTokensFn<SessionToken>,
uid: string,
activeByDateTimestamp: number
) => {
const sessionTokens = await tokensFn(uid);
return sessionTokens.some(
(token) =>
token.lastAccessTime && token.lastAccessTime >= activeByDateTimestamp
);
};
export const hasActiveRefreshToken = async (
tokensFn: GetTokensFn<{ lastUsedAt: number }>,
uid: string,
activeByDateTimestamp: number
) => {
const refreshTokens = await tokensFn(uid);
return refreshTokens.some((t) => t.lastUsedAt >= activeByDateTimestamp);
};
export const hasAccessToken = async (
tokensFn: GetTokensFn<{ lastUsedAt: number }>,
uid: string
) => {
const accessTokens = await tokensFn(uid);
return accessTokens.length > 0;
};
export type ActiveConditionFn = (
uid: string
) => Promise<boolean> | Promise<Promise<boolean>>;
/**
* This simple builder exists purely to make it clear, and in one place, what
* conditions are required to consider an account (in)active, in addition to
* the DB query conditions.
*/
export class IsActiveFnBuilder {
// @TODO we need to add in the RP exclusion check here if it's not possible with MySQL
requiredFn = (message: string) => () => {
throw new Error(message);
};
activeSessionTokenFn: ActiveConditionFn;
refreshTokenFn: ActiveConditionFn;
accessTokenFn: ActiveConditionFn;
constructor() {
this.activeSessionTokenFn = this.requiredFn(
'A function to check for an active session token is required.'
);
this.refreshTokenFn = this.requiredFn(
'A function to check for a refresh token is required.'
);
this.accessTokenFn = this.requiredFn(
'A function to check for an access token is required.'
);
}
setActiveSessionTokenFn(fn: ActiveConditionFn) {
this.activeSessionTokenFn = fn;
return this;
}
setRefreshTokenFn(fn: ActiveConditionFn) {
this.refreshTokenFn = fn;
return this;
}
setAccessTokenFn(fn: ActiveConditionFn) {
this.accessTokenFn = fn;
return this;
}
build() {
return (async (uid: string) =>
(await this.activeSessionTokenFn(uid)) ||
(await this.refreshTokenFn(uid)) ||
(await this.accessTokenFn(uid))).bind(this);
}
}
export const buildExclusionsTempTableQuery = (
tempTableName: string,
exclusionLists: string[]
) => {
const createTempTable = `CREATE TEMP TABLE ${tempTableName}(uid STRING(32))`;
if (!exclusionLists.length) {
return createTempTable;
}
const listQueries = exclusionLists.map((resourcePath) => {
const parts = resourcePath.split('.');
const columnName = parts[parts.length - 1];
const resourceId = parts.slice(0, parts.length - 1).join('.');
return `
(SELECT \`${columnName}\` AS uid FROM \`${resourceId}\`)
`;
});
return `${createTempTable}
AS (
${listQueries.join(`
UNION DISTINCT
`)}
)`;
};