2024-05-07 12:04:03 +09:00

638 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { app, InvocationContext, Timer } from "@azure/functions";
import { Between, DataSource, In, IsNull, MoreThan, Not } from "typeorm";
import { Account } from "../entity/account.entity";
import {
ADB2C_SIGN_IN_TYPE,
LICENSE_ALLOCATED_STATUS,
TIERS,
} from "../constants";
import * as dotenv from "dotenv";
import { License } from "../entity/license.entity";
import {
DateWithDayEndTime,
DateWithZeroTime,
ExpirationThresholdDate,
} from "../common/types/types";
import { createMailContentOfLicenseShortage } from "../sendgrid/mailContents/U103ShortageAlert";
import { createMailContentOfLicenseExpiringSoon } from "../sendgrid/mailContents/U104ExpiringSoonAlert";
import { AdB2cService } from "../adb2c/adb2c";
import { SendGridService } from "../sendgrid/sendgrid";
import { getMailFrom } from "../common/getEnv/getEnv";
import { createRedisClient } from "../redis/redis";
import { RedisClient } from "redis";
import { promisify } from "util";
import { makeSendCompKey } from "../common/cache";
import {
MAIL_U103,
MAIL_U104,
SEND_COMPLETE_PREFIX,
DONE,
} from "../common/cache/constants";
import { initializeDataSource } from "../database/initializeDataSource";
export async function licenseAlertProcessing(
context: InvocationContext,
datasource: DataSource,
redisClient: RedisClient,
sendgrid: SendGridService,
adb2c: AdB2cService
) {
try {
context.log("[IN]licenseAlertProcessing");
// redisのキー用
const currentDate = new DateWithZeroTime();
const formattedDate = `${currentDate.getFullYear()}-${(
currentDate.getMonth() + 1
).toString()}-${currentDate.getDate().toString()}`;
const keysAsync = promisify(redisClient.keys).bind(redisClient);
// メール送信対象のアカウント情報を取得
const sendTargetAccounts = await getAlertMailTargetAccount(
context,
datasource
);
// adb2cからメールアドレスを取得し、上記で取得したアカウントにマージする
const sendTargetAccountsMargedAdb2c = await createAccountInfo(
context,
redisClient,
adb2c,
sendTargetAccounts
);
// メール送信
await sendAlertMail(
context,
redisClient,
sendgrid,
sendTargetAccountsMargedAdb2c,
formattedDate
);
// 最後まで処理が正常に通ったら、redisに保存した送信情報を削除する
try {
const delAsync = promisify(redisClient.del).bind(redisClient);
const keys = await keysAsync(`${SEND_COMPLETE_PREFIX}${formattedDate}*`);
console.log(`delete terget:${keys}`);
if (keys.length > 0) {
const delResult = await delAsync(keys);
console.log(`delete number:${delResult}`);
}
} catch (e) {
context.log("redis delete failed");
throw e;
}
} catch (e) {
throw e;
} finally {
context.log("[OUT]licenseAlertProcessing");
}
}
export async function licenseAlert(
myTimer: Timer,
context: InvocationContext
): Promise<void> {
context.log("[IN]licenseAlert");
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.local", override: true });
const datasource = await initializeDataSource(context);
let redisClient: RedisClient;
try {
// redis接続
redisClient = createRedisClient();
} catch (e) {
context.log("redis client create failed.");
context.error(e);
throw e;
}
try {
const adb2c = new AdB2cService();
const sendgrid = new SendGridService();
await licenseAlertProcessing(
context,
datasource,
redisClient,
sendgrid,
adb2c
);
} catch (e) {
context.log("licenseAlertProcessing failed.");
context.error(e);
throw e;
} finally {
await datasource.destroy();
redisClient.quit;
context.log("[OUT]licenseAlert");
}
}
/**
* アラートメールを送信する対象のアカウントを取得する
* @param context
* @param datasource
* @returns accountInfo[] メール送信対象のアカウント情報
*/
async function getAlertMailTargetAccount(
context: InvocationContext,
datasource: DataSource
): Promise<accountInfo[]> {
try {
context.log("[IN]getAlertMailTargetAccount");
const currentDate = new DateWithZeroTime();
const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime());
const currentDateWithZeroTime = new DateWithZeroTime();
const currentDateWithDayEndTime = new DateWithDayEndTime();
// 第五のアカウントを取得
const accountRepository = datasource.getRepository(Account);
// 管理者ユーザーが規約に同意しているアカウントのみを取得
// 第五階層が同意する規約はeulaとprivacy_noticeのみであるため、それらがnullでないことを条件に追加
// 管理者が規約同意していない = 一度もログインしていないアカウントであるため、そのアカウントにはアラートメールを送信しない
// プロダクト バックログ項目 4073: [保守]システムに一度もログインしていないユーザーにはライセンスアラートメールを送信しないようにしたい の対応
const accounts = await accountRepository.find({
where: {
tier: TIERS.TIER5,
primaryAdminUser: {
accepted_eula_version: Not(IsNull()),
accepted_privacy_notice_version: Not(IsNull()),
},
},
relations: {
primaryAdminUser: true,
secondaryAdminUser: true,
},
});
const sendTargetAccounts = [] as accountInfo[];
const licenseRepository = datasource.getRepository(License);
for (const account of accounts) {
// 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
const allocatableLicenseWithMargin = await licenseRepository.count({
where: [
{
account_id: account.id,
status: In([
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
LICENSE_ALLOCATED_STATUS.REUSABLE,
]),
expiry_date: MoreThan(expiringSoonDate),
},
{
account_id: account.id,
status: In([
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
LICENSE_ALLOCATED_STATUS.REUSABLE,
]),
expiry_date: IsNull(),
},
],
});
// 有効期限が現在日付からしきい値以内のライセンス数を取得する
const expiringSoonLicense = await licenseRepository.count({
where: {
account_id: account.id,
expiry_date: Between(currentDate, expiringSoonDate),
status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
},
});
// shortage算出
let shortage = allocatableLicenseWithMargin - expiringSoonLicense;
shortage = shortage >= 0 ? 0 : Math.abs(shortage);
// AutoRenewが未チェックかつ、有効期限当日のライセンスが割り当てられているユーザー数を取得
const userCount = await licenseRepository.count({
where: [
{
account_id: account.id,
expiry_date: Between(
currentDateWithZeroTime,
currentDateWithDayEndTime
),
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
user: {
auto_renew: false,
},
},
],
relations: {
user: true,
},
});
// 上で取得したshortageとユーザー数のどちらかが1以上ならプライマリ、セカンダリ管理者、親企業名を保持する
// shortageとユーザー数のどちらかが1以上 = アラートメールを送る必要がある)
let primaryAdminExternalId: string | undefined;
let secondaryAdminExternalId: string | undefined;
let parentCompanyName: string | undefined;
if (shortage !== 0 || userCount !== 0) {
primaryAdminExternalId = account.primaryAdminUser
? account.primaryAdminUser.external_id
: undefined;
secondaryAdminExternalId = account.secondaryAdminUser
? account.secondaryAdminUser.external_id
: undefined;
// 第五のアカウントを取得
// strictNullChecks対応
if (account.parent_account_id) {
const parent = await accountRepository.findOne({
where: {
id: account.parent_account_id,
},
});
parentCompanyName = parent?.company_name;
}
} else {
primaryAdminExternalId = undefined;
secondaryAdminExternalId = undefined;
parentCompanyName = undefined;
}
sendTargetAccounts.push({
accountId: account.id,
companyName: account.company_name,
parentCompanyName: parentCompanyName,
shortage: shortage,
userCountOfLicenseExpiringSoon: userCount,
primaryAdminExternalId: primaryAdminExternalId,
secondaryAdminExternalId: secondaryAdminExternalId,
primaryAdminEmail: undefined,
secondaryAdminEmail: undefined,
});
}
return sendTargetAccounts;
} catch (e) {
context.error(e);
context.log("getAlertMailTargetAccount failed.");
throw e;
} finally {
context.log("[OUT]getAlertMailTargetAccount");
}
}
/**
* Azure AD B2Cからユーザ情報を取得し、アカウント情報を作成する
* @param context
* @param redisClient
* @param adb2c
* @param sendTargetAccounts RDBから取得したアカウント情報
* @returns accountInfo[] メール送信対象のアカウント情報
*/
async function createAccountInfo(
context: InvocationContext,
redisClient: RedisClient,
adb2c: AdB2cService,
sendTargetAccounts: accountInfo[]
): Promise<accountInfo[]> {
// ADB2Cからユーザーを取得する用の外部ID配列を作成
const externalIds = [] as string[];
sendTargetAccounts.forEach((x) => {
if (x.primaryAdminExternalId) {
externalIds.push(x.primaryAdminExternalId);
}
if (x.secondaryAdminExternalId) {
externalIds.push(x.secondaryAdminExternalId);
}
});
const adb2cUsers = await adb2c.getUsers(context, redisClient, externalIds);
if (adb2cUsers.length === 0) {
context.log("Target user not found");
return [];
}
// ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ
sendTargetAccounts.map((info) => {
const primaryAdminUser = adb2cUsers.find(
(adb2c) => info.primaryAdminExternalId === adb2c.id
);
if (primaryAdminUser) {
const primaryAdminMail = primaryAdminUser.identities?.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS
)?.issuerAssignedId;
if (primaryAdminMail) {
info.primaryAdminEmail = primaryAdminMail;
}
const secondaryAdminUser = adb2cUsers.find(
(adb2c) => info.secondaryAdminExternalId === adb2c.id
);
if (secondaryAdminUser) {
const secondaryAdminMail = secondaryAdminUser.identities?.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS
)?.issuerAssignedId;
if (secondaryAdminMail) {
info.secondaryAdminEmail = secondaryAdminMail;
}
}
}
});
return sendTargetAccounts;
}
/**
* アラートメールを送信する
* @param context
* @param redisClient
* @param sendgrid
* @param sendTargetAccounts メール送信対象のアカウント情報
* @param formattedDate redisのキーに使用する日付
* @returns ユーザ情報
*/
async function sendAlertMail(
context: InvocationContext,
redisClient: RedisClient,
sendgrid: SendGridService,
sendTargetAccounts: accountInfo[],
formattedDate: string
): Promise<void> {
try {
context.log("[IN]sendAlertMail");
// redis用
const getAsync = promisify(redisClient.get).bind(redisClient);
const setexAsync = promisify(redisClient.setex).bind(redisClient);
const ttl = process.env.ADB2C_CACHE_TTL;
const mailFrom = getMailFrom();
for (const targetAccount of sendTargetAccounts) {
// プライマリ管理者が入っているかチェック
// 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ
if (targetAccount.primaryAdminExternalId) {
// メール送信
// strictNullChecks対応
if (!targetAccount.primaryAdminEmail) {
continue;
}
// ライセンス不足メール
if (targetAccount.shortage !== 0) {
// redisに送信履歴がない場合のみ送信する
const mailResult = await getAsync(
makeSendCompKey(
formattedDate,
targetAccount.primaryAdminExternalId,
MAIL_U103
)
);
if (mailResult !== DONE) {
const { subject, text, html } =
await createMailContentOfLicenseShortage(
targetAccount.companyName,
targetAccount.shortage,
targetAccount.parentCompanyName
);
// メールを送信
try {
await sendgrid.sendMail(
context,
[targetAccount.primaryAdminEmail],
[],
mailFrom,
subject,
text,
html
);
context.log(
`Shortage mail send success. mail to :${targetAccount.primaryAdminEmail}`
);
// 送信成功時、成功履歴をredisに保存
try {
const key = makeSendCompKey(
formattedDate,
targetAccount.primaryAdminExternalId,
MAIL_U103
);
await setexAsync(key, ttl, DONE);
context.log(
"setex Result:",
`key:${key},ttl:${ttl},value:Done`
);
} catch (e) {
context.error(e);
context.log(
"setex failed.",
`target: ${targetAccount.primaryAdminEmail}`
);
}
} catch (e) {
context.error(e);
context.log(
`Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}`
);
throw e;
}
}
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
if (
targetAccount.secondaryAdminEmail &&
targetAccount.secondaryAdminExternalId
) {
// redisに送信履歴がない場合のみ送信する
const mailResult = await getAsync(
makeSendCompKey(
formattedDate,
targetAccount.secondaryAdminExternalId,
MAIL_U103
)
);
if (mailResult !== DONE) {
const { subject, text, html } =
await createMailContentOfLicenseShortage(
targetAccount.companyName,
targetAccount.shortage,
targetAccount.parentCompanyName
);
// メールを送信
try {
await sendgrid.sendMail(
context,
[targetAccount.secondaryAdminEmail],
[],
mailFrom,
subject,
text,
html
);
context.log(
`Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}`
);
// 送信成功時、成功履歴をredisに保存
try {
const key = makeSendCompKey(
formattedDate,
targetAccount.secondaryAdminExternalId,
MAIL_U103
);
await setexAsync(key, ttl, DONE);
context.log(
"setex Result:",
`key:${key},ttl:${ttl},value:Done`
);
} catch (e) {
context.error(e);
context.log(
"setex failed.",
`target: ${targetAccount.secondaryAdminEmail}`
);
}
} catch (e) {
context.error(e);
context.log(
`Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
);
throw e;
}
}
}
}
// ライセンス失効警告メール
if (targetAccount.userCountOfLicenseExpiringSoon !== 0) {
// redisに送信履歴がない場合のみ送信する
const mailResult = await getAsync(
makeSendCompKey(
formattedDate,
targetAccount.primaryAdminExternalId,
MAIL_U104
)
);
if (mailResult !== DONE) {
const { subject, text, html } =
await createMailContentOfLicenseExpiringSoon(
targetAccount.companyName,
targetAccount.userCountOfLicenseExpiringSoon,
targetAccount.parentCompanyName
);
// メールを送信
try {
await sendgrid.sendMail(
context,
[targetAccount.primaryAdminEmail],
[],
mailFrom,
subject,
text,
html
);
context.log(
`Expiring soon mail send success. mail to :${targetAccount.primaryAdminEmail}`
);
// 送信成功時、成功履歴をredisに保存
try {
const key = makeSendCompKey(
formattedDate,
targetAccount.primaryAdminExternalId,
MAIL_U104
);
await setexAsync(key, ttl, DONE);
context.log(
"setex Result:",
`key:${key},ttl:${ttl},value:Done`
);
} catch (e) {
context.error(e);
context.log(
"setex failed.",
`target: ${targetAccount.primaryAdminEmail}`
);
}
} catch (e) {
context.error(e);
context.log(
`Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}`
);
throw e;
}
}
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
if (
targetAccount.secondaryAdminEmail &&
targetAccount.secondaryAdminExternalId
) {
// redisに送信履歴がない場合のみ送信する
const mailResult = makeSendCompKey(
formattedDate,
targetAccount.secondaryAdminExternalId,
MAIL_U104
);
if (mailResult !== DONE) {
const { subject, text, html } =
await createMailContentOfLicenseExpiringSoon(
targetAccount.companyName,
targetAccount.userCountOfLicenseExpiringSoon,
targetAccount.parentCompanyName
);
// メールを送信
try {
await sendgrid.sendMail(
context,
[targetAccount.secondaryAdminEmail],
[],
mailFrom,
subject,
text,
html
);
context.log(
`Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}`
);
try {
const key = makeSendCompKey(
formattedDate,
targetAccount.secondaryAdminExternalId,
MAIL_U104
);
await setexAsync(key, ttl, DONE);
context.log(
"setex Result:",
`key:${key},ttl:${ttl},value:Done`
);
} catch (e) {
context.error(e);
context.log(
"setex failed.",
`target: ${targetAccount.secondaryAdminEmail}`
);
}
} catch (e) {
context.error(e);
context.log(
`Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
);
throw e;
}
}
}
}
}
}
} catch (e) {
context.log("sendAlertMail failed.");
throw e;
} finally {
context.log("[OUT]sendAlertMail");
}
}
app.timer("licenseAlert", {
schedule: "0 0 1 * * *",
handler: licenseAlert,
});
class accountInfo {
accountId: number;
companyName: string;
parentCompanyName: string | undefined;
shortage: number;
userCountOfLicenseExpiringSoon: number;
primaryAdminExternalId: string | undefined;
secondaryAdminExternalId: string | undefined;
primaryAdminEmail: string | undefined;
secondaryAdminEmail: string | undefined;
}