diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss
index a1ff500..f5399f2 100644
--- a/dictation_client/src/styles/app.module.scss
+++ b/dictation_client/src/styles/app.module.scss
@@ -2306,8 +2306,7 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
- center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@@ -2318,8 +2317,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
- right center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
+ center;
background-size: 1.3rem;
}
.formChange > p {
@@ -2472,8 +2471,7 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
- center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@@ -2484,8 +2482,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
- background: #e6e6e6 url(../images/arrow_circle_right.svg) no-repeat right
- center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
+ right center;
background-size: 1.3rem;
}
.formChange > p {
diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json
index 50957fc..71b532f 100644
--- a/dictation_client/src/translation/de.json
+++ b/dictation_client/src/translation/de.json
@@ -25,6 +25,7 @@
"headerDictations": "(de)Dictations",
"headerWorkflow": "(de)Workflow",
"headerPartners": "(de)Partners",
+ "headerSupport": "(de)Support",
"tier1": "(de)Admin",
"tier2": "(de)BC",
"tier3": "(de)Distributor",
@@ -250,7 +251,11 @@
"poolTranscriptionist": "Transkriptionsliste",
"fileBackup": "(de)File Backup",
"downloadForBackup": "(de)Download for backup",
- "cancelDictation": "(de)Cancel Dictation"
+ "applications": "(de)Applications",
+ "cancelDictation": "(de)Cancel Dictation",
+ "general": "(de)General",
+ "job": "(de)Job",
+ "close": "(de)Close"
}
},
"cardLicenseIssuePopupPage": {
@@ -527,9 +532,31 @@
"title": "(de)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(de)Click here to read the terms of use.",
"linkOfDpa": "(de)Click here to read the terms of use.",
+ "linkOfPrivacyNotice": "(de)Click here to read the terms of use.",
"checkBoxForConsent": "(de)Yes, I agree to the terms of use.",
"forOdds": "(de)for ODDS.",
- "button": "(de)Continue"
+ "button": "(de)Continue",
+ "linkOfPrivacyNotice": "(de)Click here to read the terms of use."
+ }
+ },
+ "supportPage": {
+ "label": {
+ "title": "(de)Support",
+ "howToUse": "(de)How to use the system",
+ "supportPageEnglish": "OMDS Cloud User Guide",
+ "supportPageGerman": "OMDS Cloud Benutzerhandbuch",
+ "supportPageFrench": "OMDS Cloud Mode d'emploi",
+ "supportPageSpanish": "OMDS Cloud Guía del usuario"
+ },
+ "text": {
+ "notResolved": "(de)If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
+ }
+ },
+ "filePropertyPopup": {
+ "label": {
+ "general": "(de)General",
+ "job": "(de)Job",
+ "close": "(de)Close"
}
}
}
diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json
index 0bde925..cbf9f66 100644
--- a/dictation_client/src/translation/en.json
+++ b/dictation_client/src/translation/en.json
@@ -25,6 +25,7 @@
"headerDictations": "Dictations",
"headerWorkflow": "Workflow",
"headerPartners": "Partners",
+ "headerSupport": "Support",
"tier1": "Admin",
"tier2": "BC",
"tier3": "Distributor",
@@ -250,7 +251,11 @@
"poolTranscriptionist": "Transcription List",
"fileBackup": "File Backup",
"downloadForBackup": "Download for backup",
- "cancelDictation": "Cancel Dictation"
+ "applications": "Applications",
+ "cancelDictation": "Cancel Dictation",
+ "general": "General",
+ "job": "Job",
+ "close": "Close"
}
},
"cardLicenseIssuePopupPage": {
@@ -527,9 +532,31 @@
"title": "Terms of Use has updated. Please confirm again.",
"linkOfEula": "Click here to read the terms of use.",
"linkOfDpa": "Click here to read the terms of use.",
+ "linkOfPrivacyNotice": "Click here to read the terms of use.",
"checkBoxForConsent": "Yes, I agree to the terms of use.",
"forOdds": "for ODDS.",
- "button": "Continue"
+ "button": "Continue",
+ "linkOfPrivacyNotice": "Click here to read the terms of use."
+ }
+ },
+ "supportPage": {
+ "label": {
+ "title": "Support",
+ "howToUse": "How to use the system",
+ "supportPageEnglish": "OMDS Cloud User Guide",
+ "supportPageGerman": "OMDS Cloud Benutzerhandbuch",
+ "supportPageFrench": "OMDS Cloud Mode d'emploi",
+ "supportPageSpanish": "OMDS Cloud Guía del usuario"
+ },
+ "text": {
+ "notResolved": "If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
+ }
+ },
+ "filePropertyPopup": {
+ "label": {
+ "general": "General",
+ "job": "Job",
+ "close": "Close"
}
}
}
diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json
index 481c79d..552edb2 100644
--- a/dictation_client/src/translation/es.json
+++ b/dictation_client/src/translation/es.json
@@ -25,6 +25,7 @@
"headerDictations": "(es)Dictations",
"headerWorkflow": "(es)Workflow",
"headerPartners": "(es)Partners",
+ "headerSupport": "(es)Support",
"tier1": "(es)Admin",
"tier2": "(es)BC",
"tier3": "(es)Distributor",
@@ -250,7 +251,11 @@
"poolTranscriptionist": "Lista de transcriptor",
"fileBackup": "(es)File Backup",
"downloadForBackup": "(es)Download for backup",
- "cancelDictation": "(es)Cancel Dictation"
+ "applications": "(es)Applications",
+ "cancelDictation": "(es)Cancel Dictation",
+ "general": "(es)General",
+ "job": "(es)Job",
+ "close": "(es)Close"
}
},
"cardLicenseIssuePopupPage": {
@@ -527,9 +532,31 @@
"title": "(es)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(es)Click here to read the terms of use.",
"linkOfDpa": "(es)Click here to read the terms of use.",
+ "linkOfPrivacyNotice": "(es)Click here to read the terms of use.",
"checkBoxForConsent": "(es)Yes, I agree to the terms of use.",
"forOdds": "(es)for ODDS.",
- "button": "(es)Continue"
+ "button": "(es)Continue",
+ "linkOfPrivacyNotice": "(es)Click here to read the terms of use."
+ }
+ },
+ "supportPage": {
+ "label": {
+ "title": "(es)Support",
+ "howToUse": "(es)How to use the system",
+ "supportPageEnglish": "OMDS Cloud User Guide",
+ "supportPageGerman": "OMDS Cloud Benutzerhandbuch",
+ "supportPageFrench": "OMDS Cloud Mode d'emploi",
+ "supportPageSpanish": "OMDS Cloud Guía del usuario"
+ },
+ "text": {
+ "notResolved": "(es)If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
+ }
+ },
+ "filePropertyPopup": {
+ "label": {
+ "general": "(es)General",
+ "job": "(es)Job",
+ "close": "(es)Close"
}
}
}
diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json
index 6ffc1bd..e3bf194 100644
--- a/dictation_client/src/translation/fr.json
+++ b/dictation_client/src/translation/fr.json
@@ -25,6 +25,7 @@
"headerDictations": "(fr)Dictations",
"headerWorkflow": "(fr)Workflow",
"headerPartners": "(fr)Partners",
+ "headerSupport": "(fr)Support",
"tier1": "(fr)Admin",
"tier2": "(fr)BC",
"tier3": "(fr)Distributor",
@@ -250,7 +251,11 @@
"poolTranscriptionist": "Liste de transcriptionniste",
"fileBackup": "(fr)File Backup",
"downloadForBackup": "(fr)Download for backup",
- "cancelDictation": "(fr)Cancel Dictation"
+ "applications": "(fr)Applications",
+ "cancelDictation": "(fr)Cancel Dictation",
+ "general": "(fr)General",
+ "job": "(fr)Job",
+ "close": "(fr)Close"
}
},
"cardLicenseIssuePopupPage": {
@@ -527,9 +532,31 @@
"title": "(fr)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(fr)Click here to read the terms of use.",
"linkOfDpa": "(fr)Click here to read the terms of use.",
+ "linkOfPrivacyNotice": "(fr)Click here to read the terms of use.",
"checkBoxForConsent": "(fr)Yes, I agree to the terms of use.",
"forOdds": "(fr)for ODDS.",
- "button": "(fr)Continue"
+ "button": "(fr)Continue",
+ "linkOfPrivacyNotice": "(fr)Click here to read the terms of use."
+ }
+ },
+ "supportPage": {
+ "label": {
+ "title": "(fr)Support",
+ "howToUse": "(fr)How to use the system",
+ "supportPageEnglish": "OMDS Cloud User Guide",
+ "supportPageGerman": "OMDS Cloud Benutzerhandbuch",
+ "supportPageFrench": "OMDS Cloud Mode d'emploi",
+ "supportPageSpanish": "OMDS Cloud Guía del usuario"
+ },
+ "text": {
+ "notResolved": "(fr)If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
+ }
+ },
+ "filePropertyPopup": {
+ "label": {
+ "general": "(fr)General",
+ "job": "(fr)Job",
+ "close": "(fr)Close"
}
}
}
diff --git a/dictation_function/Dockerfile b/dictation_function/Dockerfile
deleted file mode 100644
index 3dcb7b2..0000000
--- a/dictation_function/Dockerfile
+++ /dev/null
@@ -1,12 +0,0 @@
-# To enable ssh & remote debugging on app service change the base image to the one below
-# FROM mcr.microsoft.com/azure-functions/node:4-node18-appservice
-FROM mcr.microsoft.com/azure-functions/node:4-node18
-
-ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
- AzureFunctionsJobHost__Logging__Console__IsEnabled=true
-
-COPY . /home/site/wwwroot
-
-RUN cd /home/site/wwwroot && \
- npm install && \
- npm run build
\ No newline at end of file
diff --git a/dictation_function/host.json b/dictation_function/host.json
index 9df9136..a75de3b 100644
--- a/dictation_function/host.json
+++ b/dictation_function/host.json
@@ -11,5 +11,10 @@
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
+ },
+ "retry": {
+ "strategy": "fixedDelay",
+ "maxRetryCount": 3,
+ "delayInterval": "00:00:10"
}
-}
\ No newline at end of file
+}
diff --git a/dictation_function/src/adb2c/adb2c.ts b/dictation_function/src/adb2c/adb2c.ts
index f58f9b3..b9bde0c 100644
--- a/dictation_function/src/adb2c/adb2c.ts
+++ b/dictation_function/src/adb2c/adb2c.ts
@@ -5,8 +5,8 @@ import { AdB2cResponse, AdB2cUser } from "./types/types";
import { error } from "console";
import { makeADB2CKey, restoreAdB2cID } from "../common/cache";
import { promisify } from "util";
-import { createRedisClient } from "../redis/redis";
import { InvocationContext } from "@azure/functions";
+import { RedisClient } from "redis";
export class Adb2cTooManyRequestsError extends Error {}
@@ -23,16 +23,24 @@ export class AdB2cService {
) {
throw error;
}
- const credential = new ClientSecretCredential(
- process.env.ADB2C_TENANT_ID,
- process.env.ADB2C_CLIENT_ID,
- process.env.ADB2C_CLIENT_SECRET
- );
- const authProvider = new TokenCredentialAuthenticationProvider(credential, {
- scopes: ["https://graph.microsoft.com/.default"],
- });
+ try {
+ const credential = new ClientSecretCredential(
+ process.env.ADB2C_TENANT_ID,
+ process.env.ADB2C_CLIENT_ID,
+ process.env.ADB2C_CLIENT_SECRET
+ );
+ const authProvider = new TokenCredentialAuthenticationProvider(
+ credential,
+ {
+ scopes: ["https://graph.microsoft.com/.default"],
+ }
+ );
- this.graphClient = Client.initWithMiddleware({ authProvider });
+ this.graphClient = Client.initWithMiddleware({ authProvider });
+ } catch (error) {
+ console.log(error);
+ throw error;
+ }
}
/**
@@ -42,9 +50,9 @@ export class AdB2cService {
*/
async getUsers(
context: InvocationContext,
+ redisClient: RedisClient,
externalIds: string[]
- ): Promise {
- const redisClient = createRedisClient();
+ ): Promise {
try {
const b2cUsers: AdB2cUser[] = [];
const keys = externalIds.map((externalId) => makeADB2CKey(externalId));
@@ -123,7 +131,7 @@ export class AdB2cService {
return [...cachedUsers, ...b2cUsers];
} else {
- return undefined;
+ return b2cUsers;
}
} catch (e) {
const { statusCode } = e;
@@ -132,7 +140,6 @@ export class AdB2cService {
}
throw e;
} finally {
- redisClient.quit;
}
}
}
diff --git a/dictation_function/src/common/cache/constants.ts b/dictation_function/src/common/cache/constants.ts
index da6c13e..ffa7fe7 100644
--- a/dictation_function/src/common/cache/constants.ts
+++ b/dictation_function/src/common/cache/constants.ts
@@ -1 +1,5 @@
-export const ADB2C_PREFIX = "adb2c-external-id:"
\ No newline at end of file
+export const ADB2C_PREFIX = "adb2c-external-id:";
+export const SEND_COMPLETE_PREFIX = "send-complete-id:";
+export const MAIL_U103 = "[U103]";
+export const MAIL_U104 = "[U104]";
+export const DONE = "Done"; // メール送信成功時にredisにキャッシュする値
diff --git a/dictation_function/src/common/cache/index.ts b/dictation_function/src/common/cache/index.ts
index 067be25..27c0eab 100644
--- a/dictation_function/src/common/cache/index.ts
+++ b/dictation_function/src/common/cache/index.ts
@@ -1,4 +1,4 @@
-import { ADB2C_PREFIX } from './constants';
+import { ADB2C_PREFIX, SEND_COMPLETE_PREFIX } from "./constants";
/**
* ADB2Cのユーザー格納用のキーを生成する
@@ -6,8 +6,8 @@ import { ADB2C_PREFIX } from './constants';
* @returns キャッシュのキー
*/
export const makeADB2CKey = (externalId: string): string => {
- return `${ADB2C_PREFIX}${externalId}`;
-}
+ return `${ADB2C_PREFIX}${externalId}`;
+};
/**
* ADB2Cのユーザー格納用のキーから外部ユーザーIDを取得する
@@ -15,5 +15,20 @@ export const makeADB2CKey = (externalId: string): string => {
* @returns 外部ユーザーID
*/
export const restoreAdB2cID = (key: string): string => {
- return key.replace(ADB2C_PREFIX, '');
-}
\ No newline at end of file
+ return key.replace(ADB2C_PREFIX, "");
+};
+
+/**
+ * ライセンスアラートメール送信履歴格納用のキーを生成する
+ * @param formattedDate 当日の日付(YYYY:MM:DD)
+ * @param externalId 外部ユーザーID
+ * @param mail メール種別
+ * @returns キャッシュのキー
+ */
+export const makeSendCompKey = (
+ formattedDate: string,
+ externalId: string,
+ mail: string
+): string => {
+ return `${SEND_COMPLETE_PREFIX}${formattedDate}${mail}${externalId}`;
+};
diff --git a/dictation_function/src/functions/licenseAlert.ts b/dictation_function/src/functions/licenseAlert.ts
index f8f2a71..bbab8b0 100644
--- a/dictation_function/src/functions/licenseAlert.ts
+++ b/dictation_function/src/functions/licenseAlert.ts
@@ -19,36 +19,166 @@ import { createMailContentOfLicenseExpiringSoon } from "../sendgrid/mailContents
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";
export async function licenseAlertProcessing(
context: InvocationContext,
datasource: DataSource,
+ redisClient: RedisClient,
sendgrid: SendGridService,
adb2c: AdB2cService
) {
- context.log("[IN]licenseAlertProcessing");
- const mailFrom = getMailFrom();
- const accountRepository = datasource.getRepository(Account);
+ try {
+ context.log("[IN]licenseAlertProcessing");
- // 第五のアカウントを取得
- const accounts = await accountRepository.find({
- where: {
- tier: TIERS.TIER5,
- },
- relations: {
- primaryAdminUser: true,
- secondaryAdminUser: true,
- },
- });
+ // redisのキー用
+ const currentDate = new DateWithZeroTime();
+ const formattedDate = `${currentDate.getFullYear()}-${(
+ currentDate.getMonth() + 1
+ ).toString()}-${currentDate.getDate().toString()}`;
+ const keysAsync = promisify(redisClient.keys).bind(redisClient);
- const licenseRepository = datasource.getRepository(License);
- const currentDate = new DateWithZeroTime();
- const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime());
- const currentDateWithZeroTime = new DateWithZeroTime();
- const currentDateWithDayEndTime = new DateWithDayEndTime();
- const sendTargetAccounts = [] as accountInfo[];
+ // メール送信対象のアカウント情報を取得
+ const sendTargetAccounts = await getAlertMailTargetAccount(
+ context,
+ datasource
+ );
- const counts = async () => {
+ // 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 {
+ context.log("[IN]licenseAlert");
+
+ dotenv.config({ path: ".env" });
+ dotenv.config({ path: ".env.local", override: true });
+ let datasource: DataSource;
+ try {
+ datasource = new DataSource({
+ type: "mysql",
+ host: process.env.DB_HOST,
+ port: Number(process.env.DB_PORT),
+ username: process.env.DB_USERNAME,
+ password: process.env.DB_PASSWORD,
+ database: process.env.DB_NAME,
+ entities: [User, Account, License],
+ });
+ await datasource.initialize();
+ } catch (e) {
+ context.log("database initialize failed.");
+ context.error(e);
+ throw e;
+ }
+
+ 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 {
+ 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);
+ const accounts = await accountRepository.find({
+ where: {
+ tier: TIERS.TIER5,
+ },
+ relations: {
+ primaryAdminUser: true,
+ secondaryAdminUser: true,
+ },
+ });
+
+ const sendTargetAccounts = [] as accountInfo[];
+ const licenseRepository = datasource.getRepository(License);
for (const account of accounts) {
// 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
const allocatableLicenseWithMargin = await licenseRepository.count({
@@ -109,6 +239,7 @@ export async function licenseAlertProcessing(
let primaryAdminExternalId: string | undefined;
let secondaryAdminExternalId: string | undefined;
let parentCompanyName: string | undefined;
+
if (shortage !== 0 || userCount !== 0) {
primaryAdminExternalId = account.primaryAdminUser
? account.primaryAdminUser.external_id
@@ -143,12 +274,33 @@ export async function licenseAlertProcessing(
secondaryAdminEmail: undefined,
});
}
- };
- await counts();
+ 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 {
// ADB2Cからユーザーを取得する用の外部ID配列を作成
const externalIds = [] as string[];
- sendTargetAccounts.map((x) => {
+ sendTargetAccounts.forEach((x) => {
if (x.primaryAdminExternalId) {
externalIds.push(x.primaryAdminExternalId);
}
@@ -156,11 +308,10 @@ export async function licenseAlertProcessing(
externalIds.push(x.secondaryAdminExternalId);
}
});
- const adb2cUsers = await adb2c.getUsers(context, externalIds);
- if (!adb2cUsers) {
+ const adb2cUsers = await adb2c.getUsers(context, redisClient, externalIds);
+ if (adb2cUsers.length === 0) {
context.log("Target user not found");
- context.log("[OUT]licenseAlertProcessing");
- return;
+ return [];
}
// ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ
sendTargetAccounts.map((info) => {
@@ -188,17 +339,54 @@ export async function licenseAlertProcessing(
}
}
});
+ 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 {
+ 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();
- const sendMail = async () => {
for (const targetAccount of sendTargetAccounts) {
// プライマリ管理者が入っているかチェック
// 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ
if (targetAccount.primaryAdminExternalId) {
// メール送信
// strictNullChecks対応
- if (targetAccount.primaryAdminEmail) {
- // ライセンス不足メール
- if (targetAccount.shortage !== 0) {
+ 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,
@@ -217,45 +405,107 @@ export async function licenseAlertProcessing(
context.log(
`Shortage mail send success. mail to :${targetAccount.primaryAdminEmail}`
);
- } catch {
+ // 送信成功時、成功履歴を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}`
);
- }
-
- // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
- if (targetAccount.secondaryAdminEmail) {
- // ライセンス不足メール
- if (targetAccount.shortage !== 0) {
- const { subject, text, html } =
- await createMailContentOfLicenseShortage(
- targetAccount.companyName,
- targetAccount.shortage,
- targetAccount.parentCompanyName
- );
- // メールを送信
- try {
- await sendgrid.sendMail(
- targetAccount.secondaryAdminEmail,
- mailFrom,
- subject,
- text,
- html
- );
- context.log(
- `Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}`
- );
- } catch {
- context.log(
- `Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
- );
- }
- }
+ throw e;
}
}
- // ライセンス失効警告メール
- if (targetAccount.userCountOfLicenseExpiringSoon !== 0) {
+ // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
+ 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(
+ 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,
@@ -274,80 +524,99 @@ export async function licenseAlertProcessing(
context.log(
`Expiring soon mail send success. mail to :${targetAccount.primaryAdminEmail}`
);
- } catch {
+ // 送信成功時、成功履歴を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) {
- // ライセンス不足メール
- if (targetAccount.shortage !== 0) {
- const { subject, text, html } =
- await createMailContentOfLicenseExpiringSoon(
- targetAccount.companyName,
- targetAccount.userCountOfLicenseExpiringSoon,
- targetAccount.parentCompanyName
- );
- // メールを送信
+ // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
+ 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(
+ targetAccount.secondaryAdminEmail,
+ mailFrom,
+ subject,
+ text,
+ html
+ );
+ context.log(
+ `Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}`
+ );
try {
- await sendgrid.sendMail(
- targetAccount.secondaryAdminEmail,
- mailFrom,
- subject,
- text,
- html
+ const key = makeSendCompKey(
+ formattedDate,
+ targetAccount.secondaryAdminExternalId,
+ MAIL_U104
);
+ await setexAsync(key, ttl, DONE);
context.log(
- `Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}`
+ "setex Result:",
+ `key:${key},ttl:${ttl},value:Done`
);
- } catch {
+ } catch (e) {
+ context.error(e);
context.log(
- `Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
+ "setex failed.",
+ `target: ${targetAccount.secondaryAdminEmail}`
);
}
+ } catch (e) {
+ context.error(e);
+ context.log(
+ `Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
+ );
+ throw e;
}
}
}
}
}
}
- };
- await sendMail();
-
- context.log("[OUT]licenseAlertProcessing");
-}
-
-export async function licenseAlert(
- myTimer: Timer,
- context: InvocationContext
-): Promise {
- context.log("[IN]licenseAlert");
-
- dotenv.config({ path: ".env" });
- dotenv.config({ path: ".env.local", override: true });
- const datasource = new DataSource({
- type: "mysql",
- host: process.env.DB_HOST,
- port: Number(process.env.DB_PORT),
- username: process.env.DB_USERNAME,
- password: process.env.DB_PASSWORD,
- database: process.env.DB_NAME,
- entities: [User, Account, License],
- });
- await datasource.initialize();
-
- const adb2c = new AdB2cService();
- const sendgrid = new SendGridService();
- try {
- await licenseAlertProcessing(context, datasource, sendgrid, adb2c);
} catch (e) {
- context.log("licenseAlertProcessing failed");
- context.error(e);
+ context.log("sendAlertMail failed.");
+ throw e;
} finally {
- await datasource.destroy();
- context.log("[OUT]licenseAlert");
+ context.log("[OUT]sendAlertMail");
}
}
diff --git a/dictation_function/src/functions/redisTimerTest.ts b/dictation_function/src/functions/redisTimerTest.ts
deleted file mode 100644
index 419079d..0000000
--- a/dictation_function/src/functions/redisTimerTest.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { app, InvocationContext, Timer } from "@azure/functions";
-import * as dotenv from "dotenv";
-import { promisify } from "util";
-import { createRedisClient } from "../redis/redis";
-
-export async function redisTimerTest(
- myTimer: Timer,
- context: InvocationContext
-): Promise {
- context.log("---Timer function processed request.");
-
- dotenv.config({ path: ".env" });
- dotenv.config({ path: ".env.local", override: true });
-
- const redisClient = createRedisClient();
- const setAsync = promisify(redisClient.set).bind(redisClient);
- const getAsync = promisify(redisClient.get).bind(redisClient);
-
- await setAsync("foo", "bar");
- const value = await getAsync("foo");
- context.log(`value=${value}`); // returns 'bar'
-
- await redisClient.quit;
-}
-
-app.timer("redisTimerTest", {
- schedule: "*/30 * * * * *",
- handler: redisTimerTest,
-});
diff --git a/dictation_function/src/redis/redis.ts b/dictation_function/src/redis/redis.ts
index aedb573..c4ef775 100644
--- a/dictation_function/src/redis/redis.ts
+++ b/dictation_function/src/redis/redis.ts
@@ -20,12 +20,34 @@ export const createRedisClient = (): RedisClient => {
host: host,
port: port,
password: password,
+ retry_strategy: (options) => {
+ if (options.attempt <= 3) {
+ console.log(
+ `Retrying connection to Redis. Attempt ${options.attempt}`
+ );
+ return 10000; // ミリ秒単位でのリトライまでの間隔
+ } else {
+ console.log("Exceeded maximum number of connection attempts.");
+ return undefined; // リトライを終了
+ }
+ },
});
} else {
client = createClient({
url: `rediss://${host}:${port}`,
password: password,
tls: {},
+ retry_strategy: (options) => {
+ if (options.attempt <= 3) {
+ console.log(
+ `Retrying connection to Redis. Attempt ${options.attempt}`
+ );
+ return 10000; // ミリ秒単位でのリトライまでの間隔
+ } else {
+ console.log("Exceeded maximum number of connection attempts.");
+ return undefined; // リトライを終了
+ }
+ },
});
}
diff --git a/dictation_function/src/test/licenseAlert.spec.ts b/dictation_function/src/test/licenseAlert.spec.ts
index a37e9d4..d790cf0 100644
--- a/dictation_function/src/test/licenseAlert.spec.ts
+++ b/dictation_function/src/test/licenseAlert.spec.ts
@@ -13,6 +13,8 @@ import { ADB2C_SIGN_IN_TYPE } from "../constants";
import { SendGridService } from "../sendgrid/sendgrid";
import { AdB2cService } from "../adb2c/adb2c";
import { InvocationContext } from "@azure/functions";
+import { RedisClient } from "redis";
+import { createRedisClient } from "../redis/redis";
describe("licenseAlert", () => {
dotenv.config({ path: ".env" });
@@ -40,6 +42,7 @@ describe("licenseAlert", () => {
const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
+ const redisClient = createRedisClient();
// 呼び出し回数でテスト成否を判定
const spySend = jest.spyOn(sendgridMock, "sendMail");
@@ -63,8 +66,15 @@ describe("licenseAlert", () => {
null
);
- await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
+ await licenseAlertProcessing(
+ context,
+ source,
+ redisClient,
+ sendgridMock,
+ adb2cMock
+ );
expect(spySend.mock.calls).toHaveLength(1);
+ redisClient.quit;
});
it("ライセンス在庫不足メール、ライセンス失効警告メールが送信されること", async () => {
@@ -72,6 +82,7 @@ describe("licenseAlert", () => {
const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
+ const redisClient = createRedisClient();
// 呼び出し回数でテスト成否を判定
const spySend = jest.spyOn(sendgridMock, "sendMail");
@@ -96,8 +107,15 @@ describe("licenseAlert", () => {
null
);
- await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
+ await licenseAlertProcessing(
+ context,
+ source,
+ redisClient,
+ sendgridMock,
+ adb2cMock
+ );
expect(spySend.mock.calls).toHaveLength(2);
+ redisClient.quit;
});
it("在庫があるため、ライセンス在庫不足メールが送信されないこと", async () => {
@@ -105,6 +123,7 @@ describe("licenseAlert", () => {
const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
+ const redisClient = createRedisClient();
// 呼び出し回数でテスト成否を判定
const spySend = jest.spyOn(sendgridMock, "sendMail");
@@ -142,8 +161,15 @@ describe("licenseAlert", () => {
null
);
- await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
+ await licenseAlertProcessing(
+ context,
+ source,
+ redisClient,
+ sendgridMock,
+ adb2cMock
+ );
expect(spySend.mock.calls).toHaveLength(0);
+ redisClient.quit;
});
it("AutoRenewがtureのため、ライセンス失効警告メールが送信されないこと", async () => {
@@ -151,6 +177,7 @@ describe("licenseAlert", () => {
const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
+ const redisClient = createRedisClient();
// 呼び出し回数でテスト成否を判定
const spySend = jest.spyOn(sendgridMock, "sendMail");
@@ -175,8 +202,15 @@ describe("licenseAlert", () => {
null
);
- await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
+ await licenseAlertProcessing(
+ context,
+ source,
+ redisClient,
+ sendgridMock,
+ adb2cMock
+ );
expect(spySend.mock.calls).toHaveLength(1);
+ redisClient.quit;
});
});
@@ -211,6 +245,7 @@ export class AdB2cServiceMock {
*/
async getUsers(
context: InvocationContext,
+ redisClient: RedisClient,
externalIds: string[]
): Promise {
const AdB2cMockUsers: AdB2cUser[] = [
diff --git a/dictation_server/db/migrations/049-add_privacy_notice_column.sql b/dictation_server/db/migrations/049-add_privacy_notice_column.sql
new file mode 100644
index 0000000..74cb8dd
--- /dev/null
+++ b/dictation_server/db/migrations/049-add_privacy_notice_column.sql
@@ -0,0 +1,12 @@
+
+-- +migrate Up
+ALTER TABLE `users` ADD COLUMN `accepted_privacy_notice_version` VARCHAR(255) COMMENT '同意済みプライバシーポリシーバージョン' AFTER `accepted_eula_version`;
+ALTER TABLE `users_archive` ADD COLUMN `accepted_privacy_notice_version` VARCHAR(255) COMMENT '同意済みプライバシーポリシーバージョン' AFTER `accepted_eula_version`;
+insert into terms(terms.document_type, terms.version) values('PrivacyNotice', 'V0.1');
+commit;
+
+-- +migrate Down
+ALTER TABLE `users` DROP COLUMN `accepted_privacy_notice_version`;
+ALTER TABLE `users_archive` DROP COLUMN `accepted_privacy_notice_version`;
+delete from terms where terms.document_type = 'PrivacyNotice' and terms.version = 'V0.1';
+commit;
\ No newline at end of file
diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json
index e112ae2..069d43c 100644
--- a/dictation_server/src/api/odms/openapi.json
+++ b/dictation_server/src/api/odms/openapi.json
@@ -1455,6 +1455,52 @@
"tags": ["accounts"]
}
},
+ "/accounts/company-name": {
+ "post": {
+ "operationId": "getCompanyName",
+ "summary": "",
+ "description": "指定したアカウントの会社名を取得します",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/GetCompanyNameRequest" }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "成功時のレスポンス",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GetCompanyNameResponse"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "認証エラー",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/ErrorResponse" }
+ }
+ }
+ },
+ "500": {
+ "description": "想定外のサーバーエラー",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/ErrorResponse" }
+ }
+ }
+ }
+ },
+ "tags": ["accounts"],
+ "security": [{ "bearer": [] }]
+ }
+ },
"/users/confirm": {
"post": {
"operationId": "confirmUser",
@@ -3491,6 +3537,10 @@
"type": "string",
"description": "同意済み利用規約のバージョン(EULA)"
},
+ "acceptedPrivacyNoticeVersion": {
+ "type": "string",
+ "description": "同意済みプライバシーポリシーのバージョン"
+ },
"acceptedDpaVersion": {
"type": "string",
"description": "同意済み利用規約のバージョン(DPA)"
@@ -3504,6 +3554,7 @@
"adminMail",
"adminPassword",
"acceptedEulaVersion",
+ "acceptedPrivacyNoticeVersion",
"acceptedDpaVersion",
"token"
]
@@ -4009,6 +4060,16 @@
"properties": { "tier": { "type": "number", "description": "階層" } },
"required": ["tier"]
},
+ "GetCompanyNameRequest": {
+ "type": "object",
+ "properties": { "accountId": { "type": "number" } },
+ "required": ["accountId"]
+ },
+ "GetCompanyNameResponse": {
+ "type": "object",
+ "properties": { "companyName": { "type": "string" } },
+ "required": ["companyName"]
+ },
"ConfirmRequest": {
"type": "object",
"properties": { "token": { "type": "string" } },
@@ -4236,12 +4297,20 @@
"type": "string",
"description": "更新バージョン(EULA)"
},
+ "acceptedPrivacyNoticeVersion": {
+ "type": "string",
+ "description": "更新バージョン(PrivacyNotice)"
+ },
"acceptedDPAVersion": {
"type": "string",
"description": "更新バージョン(DPA)"
}
},
- "required": ["idToken", "acceptedEULAVersion"]
+ "required": [
+ "idToken",
+ "acceptedEULAVersion",
+ "acceptedPrivacyNoticeVersion"
+ ]
},
"UpdateAcceptedVersionResponse": { "type": "object", "properties": {} },
"GetMyUserResponse": {
diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts
index 00b19d7..18dda44 100644
--- a/dictation_server/src/common/test/utility.ts
+++ b/dictation_server/src/common/test/utility.ts
@@ -182,6 +182,8 @@ export const makeTestAccount = async (
role: d?.role ?? 'admin none',
author_id: d?.author_id ?? undefined,
accepted_eula_version: d?.accepted_eula_version ?? '1.0',
+ accepted_privacy_notice_version:
+ d?.accepted_privacy_notice_version ?? '1.0',
accepted_dpa_version: d?.accepted_dpa_version ?? '1.0',
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts
index 0f8a7eb..439c7ed 100644
--- a/dictation_server/src/constants/index.ts
+++ b/dictation_server/src/constants/index.ts
@@ -287,6 +287,7 @@ export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]';
export const TERM_TYPE = {
EULA: 'EULA',
DPA: 'DPA',
+ PRIVACY_NOTICE: 'PrivacyNotice',
} as const;
/**
diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts
index 571f4ae..619aa2b 100644
--- a/dictation_server/src/features/accounts/accounts.controller.ts
+++ b/dictation_server/src/features/accounts/accounts.controller.ts
@@ -68,6 +68,8 @@ import {
GetAccountInfoMinimalAccessResponse,
DeleteWorktypeRequestParam,
DeleteWorktypeResponse,
+ GetCompanyNameRequest,
+ GetCompanyNameResponse,
} from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards';
@@ -116,6 +118,7 @@ export class AccountsController {
adminPassword,
adminName,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
} = body;
const role = USER_ROLES.NONE;
@@ -132,6 +135,7 @@ export class AccountsController {
adminName,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
@@ -1550,4 +1554,56 @@ export class AccountsController {
);
return { tier };
}
+
+ @ApiResponse({
+ status: HttpStatus.OK,
+ type: GetCompanyNameResponse,
+ description: '成功時のレスポンス',
+ })
+ @ApiResponse({
+ status: HttpStatus.UNAUTHORIZED,
+ description: '認証エラー',
+ type: ErrorResponse,
+ })
+ @ApiResponse({
+ status: HttpStatus.INTERNAL_SERVER_ERROR,
+ description: '想定外のサーバーエラー',
+ type: ErrorResponse,
+ })
+ @ApiOperation({
+ operationId: 'getCompanyName',
+ description: '指定したアカウントの会社名を取得します',
+ })
+ @ApiBearerAuth()
+ @UseGuards(AuthGuard)
+ @UseGuards(
+ RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
+ )
+ @Post('company-name')
+ async getCompanyName(
+ @Req() req: Request,
+ @Body() body: GetCompanyNameRequest,
+ ): Promise {
+ const accessToken = retrieveAuthorizationToken(req);
+ if (!accessToken) {
+ throw new HttpException(
+ makeErrorResponse('E000107'),
+ HttpStatus.UNAUTHORIZED,
+ );
+ }
+ const decodedAccessToken = jwt.decode(accessToken, { json: true });
+ if (!decodedAccessToken) {
+ throw new HttpException(
+ makeErrorResponse('E000101'),
+ HttpStatus.UNAUTHORIZED,
+ );
+ }
+ const { userId } = decodedAccessToken as AccessToken;
+ const context = makeContext(userId);
+ const companyName = await this.accountService.getCompanyName(
+ context,
+ body.accountId,
+ );
+ return companyName;
+ }
}
diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts
index b0c2b47..27c3715 100644
--- a/dictation_server/src/features/accounts/accounts.service.spec.ts
+++ b/dictation_server/src/features/accounts/accounts.service.spec.ts
@@ -112,6 +112,7 @@ describe('createAccount', () => {
const username = 'dummy_username';
const role = 'none';
const acceptedEulaVersion = '1.0.0';
+ const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
@@ -144,6 +145,7 @@ describe('createAccount', () => {
username,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
// 作成したアカウントのIDが返ってくるか確認
@@ -161,6 +163,9 @@ describe('createAccount', () => {
expect(account?.primary_admin_user_id).toBe(user?.id);
expect(account?.secondary_admin_user_id).toBe(null);
expect(user?.accepted_eula_version).toBe(acceptedEulaVersion);
+ expect(user?.accepted_privacy_notice_version).toBe(
+ acceptedPrivacyNoticeVersion,
+ );
expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion);
expect(user?.account_id).toBe(accountId);
expect(user?.role).toBe(role);
@@ -195,6 +200,7 @@ describe('createAccount', () => {
const username = 'dummy_username';
const role = 'admin none';
const acceptedEulaVersion = '1.0.0';
+ const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
@@ -216,6 +222,7 @@ describe('createAccount', () => {
username,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
} catch (e) {
@@ -264,6 +271,7 @@ describe('createAccount', () => {
const username = 'dummy_username';
const role = 'admin none';
const acceptedEulaVersion = '1.0.0';
+ const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
@@ -286,6 +294,7 @@ describe('createAccount', () => {
username,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
} catch (e) {
@@ -318,6 +327,7 @@ describe('createAccount', () => {
const username = 'dummy_username';
const role = 'none';
const acceptedEulaVersion = '1.0.0';
+ const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
@@ -345,6 +355,7 @@ describe('createAccount', () => {
username,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
} catch (e) {
@@ -384,6 +395,7 @@ describe('createAccount', () => {
const username = 'dummy_username';
const role = 'none';
const acceptedEulaVersion = '1.0.0';
+ const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
@@ -411,6 +423,7 @@ describe('createAccount', () => {
username,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
} catch (e) {
@@ -452,6 +465,7 @@ describe('createAccount', () => {
const username = 'dummy_username';
const role = 'none';
const acceptedEulaVersion = '1.0.0';
+ const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
@@ -480,6 +494,7 @@ describe('createAccount', () => {
username,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
} catch (e) {
@@ -520,6 +535,7 @@ describe('createAccount', () => {
const username = 'dummy_username';
const role = 'none';
const acceptedEulaVersion = '1.0.0';
+ const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
@@ -551,6 +567,7 @@ describe('createAccount', () => {
username,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
} catch (e) {
@@ -593,6 +610,7 @@ describe('createAccount', () => {
const username = 'dummy_username';
const role = 'none';
const acceptedEulaVersion = '1.0.0';
+ const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
@@ -641,6 +659,7 @@ describe('createAccount', () => {
username,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
} catch (e) {
@@ -689,6 +708,7 @@ describe('createAccount', () => {
const username = 'dummy_username';
const role = 'none';
const acceptedEulaVersion = '1.0.0';
+ const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, {
@@ -734,6 +754,7 @@ describe('createAccount', () => {
username,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
} catch (e) {
@@ -6694,3 +6715,60 @@ describe('getAccountInfoMinimalAccess', () => {
}
});
});
+describe('getCompanyName', () => {
+ let source: DataSource | null = null;
+ beforeEach(async () => {
+ source = new DataSource({
+ type: 'sqlite',
+ database: ':memory:',
+ logging: false,
+ entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
+ synchronize: true, // trueにすると自動的にmigrationが行われるため注意
+ });
+ return source.initialize();
+ });
+
+ afterEach(async () => {
+ if (!source) return;
+ await source.destroy();
+ source = null;
+ });
+
+ it('アカウントIDから会社名が取得できること', async () => {
+ if (!source) fail();
+ const module = await makeTestingModule(source);
+ if (!module) fail();
+ const service = module.get(AccountsService);
+ // 第五階層のアカウント作成
+ const { account, admin } = await makeTestAccount(source, {
+ tier: 5,
+ company_name: 'testCompany',
+ });
+ const context = makeContext(admin.external_id);
+ const response = await service.getCompanyName(context, account.id);
+ expect({ companyName: 'testCompany' }).toEqual(response);
+ });
+
+ it('アカウントが存在しない場合、400エラーとなること', async () => {
+ if (!source) fail();
+ const module = await makeTestingModule(source);
+ if (!module) fail();
+ const service = module.get(AccountsService);
+ // 第五階層のアカウント作成
+ const { account, admin } = await makeTestAccount(source, {
+ tier: 5,
+ company_name: 'testCompany',
+ });
+ const context = makeContext(admin.external_id);
+ try {
+ await service.getCompanyName(context, 123);
+ } catch (e) {
+ if (e instanceof HttpException) {
+ expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
+ expect(e.getResponse()).toEqual(makeErrorResponse('E010501'));
+ } else {
+ fail();
+ }
+ }
+ });
+});
diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts
index fda08e0..668f815 100644
--- a/dictation_server/src/features/accounts/accounts.service.ts
+++ b/dictation_server/src/features/accounts/accounts.service.ts
@@ -34,6 +34,7 @@ import {
PostWorktypeOptionItem,
Author,
Partner,
+ GetCompanyNameResponse,
} from './types/types';
import {
DateWithZeroTime,
@@ -175,6 +176,7 @@ export class AccountsService {
username: string,
role: string,
acceptedEulaVersion: string,
+ acceptedPrivacyNoticeVersion: string,
acceptedDpaVersion: string,
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
this.logger.log(
@@ -184,6 +186,7 @@ export class AccountsService {
`dealerAccountId: ${dealerAccountId}, ` +
`role: ${role}, ` +
`acceptedEulaVersion: ${acceptedEulaVersion}, ` +
+ `acceptedPrivacyNoticeVersion: ${acceptedPrivacyNoticeVersion}, ` +
`acceptedDpaVersion: ${acceptedDpaVersion} };`,
);
try {
@@ -232,6 +235,7 @@ export class AccountsService {
externalUser.sub,
role,
acceptedEulaVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
account = newAccount;
@@ -2151,4 +2155,51 @@ export class AccountsService {
);
}
}
+ /**
+ * 自アカウントの会社名を取得する
+ * @param accountId
+ * @returns CompanyName
+ */
+ async getCompanyName(
+ context: Context,
+ accountId: number,
+ ): Promise {
+ this.logger.log(
+ `[IN] [${context.getTrackingId()}] ${
+ this.getCompanyName.name
+ } | params: { accountId: ${accountId}, };`,
+ );
+
+ try {
+ const { company_name } = await this.accountRepository.findAccountById(
+ accountId,
+ );
+
+ return { companyName: company_name };
+ } catch (e) {
+ this.logger.error(`[${context.getTrackingId()}] error=${e}`);
+ if (e instanceof Error) {
+ switch (e.constructor) {
+ case AccountNotFoundError:
+ throw new HttpException(
+ makeErrorResponse('E010501'),
+ HttpStatus.BAD_REQUEST,
+ );
+ default:
+ throw new HttpException(
+ makeErrorResponse('E009999'),
+ HttpStatus.INTERNAL_SERVER_ERROR,
+ );
+ }
+ }
+ throw new HttpException(
+ makeErrorResponse('E009999'),
+ HttpStatus.INTERNAL_SERVER_ERROR,
+ );
+ } finally {
+ this.logger.log(
+ `[OUT] [${context.getTrackingId()}] ${this.getCompanyName.name}`,
+ );
+ }
+ }
}
diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts
index 921bd10..276fd0e 100644
--- a/dictation_server/src/features/accounts/types/types.ts
+++ b/dictation_server/src/features/accounts/types/types.ts
@@ -45,6 +45,8 @@ export class CreateAccountRequest {
adminPassword: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(EULA)' })
acceptedEulaVersion: string;
+ @ApiProperty({ description: '同意済みプライバシーポリシーのバージョン' })
+ acceptedPrivacyNoticeVersion: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(DPA)' })
acceptedDpaVersion: string;
@ApiProperty({ description: 'reCAPTCHA Token' })
@@ -599,3 +601,13 @@ export class GetAccountInfoMinimalAccessResponse {
@ApiProperty({ description: '階層' })
tier: number;
}
+export class GetCompanyNameRequest {
+ @ApiProperty()
+ @IsInt()
+ @Type(() => Number)
+ accountId: number;
+}
+export class GetCompanyNameResponse {
+ @ApiProperty()
+ companyName: string;
+}
diff --git a/dictation_server/src/features/auth/auth.service.spec.ts b/dictation_server/src/features/auth/auth.service.spec.ts
index 4451cb0..f978889 100644
--- a/dictation_server/src/features/auth/auth.service.spec.ts
+++ b/dictation_server/src/features/auth/auth.service.spec.ts
@@ -196,6 +196,7 @@ describe('checkIsAcceptedLatestVersion', () => {
};
await createTermInfo(source, 'EULA', '1.0');
+ await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(true);
@@ -219,6 +220,7 @@ describe('checkIsAcceptedLatestVersion', () => {
};
await createTermInfo(source, 'EULA', '1.0');
+ await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(true);
@@ -242,6 +244,7 @@ describe('checkIsAcceptedLatestVersion', () => {
};
await createTermInfo(source, 'EULA', '1.1');
+ await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false);
@@ -265,6 +268,7 @@ describe('checkIsAcceptedLatestVersion', () => {
};
await createTermInfo(source, 'EULA', '1.1');
+ await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false);
@@ -288,10 +292,35 @@ describe('checkIsAcceptedLatestVersion', () => {
};
await createTermInfo(source, 'EULA', '1.0');
+ await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.1');
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false);
});
+
+ it('同意済みプライバシーポリシーが最新でないときにチェックが通らないこと(第一~第四)', async () => {
+ if (!source) fail();
+ const module = await makeTestingModule(source);
+ if (!module) fail();
+ const service = module.get(AuthService);
+ const { admin } = await makeTestAccount(source, {
+ tier: 4,
+ });
+ const context = makeContext(uuidv4());
+
+ const idToken = {
+ emails: [],
+ sub: admin.external_id,
+ exp: 0,
+ iat: 0,
+ };
+
+ await createTermInfo(source, 'EULA', '1.0');
+ await createTermInfo(source, 'PrivacyNotice', '1.1');
+ await createTermInfo(source, 'DPA', '1.0');
+ const result = await service.isAcceptedLatestVersion(context, idToken);
+ expect(result).toBe(false);
+ });
});
describe('generateDelegationRefreshToken', () => {
diff --git a/dictation_server/src/features/auth/auth.service.ts b/dictation_server/src/features/auth/auth.service.ts
index 543ca12..dbcdfc3 100644
--- a/dictation_server/src/features/auth/auth.service.ts
+++ b/dictation_server/src/features/auth/auth.service.ts
@@ -689,28 +689,38 @@ export class AuthService {
const {
acceptedEulaVersion,
latestEulaVersion,
+ acceptedPrivacyNoticeVersion,
+ latestPrivacyNoticeVersion,
acceptedDpaVersion,
latestDpaVersion,
tier,
} = await this.usersRepository.getAcceptedAndLatestVersion(idToken.sub);
- // 第五階層はEULAのみ判定
+ // 第五階層はEULAとPrivacyNoticeのみ判定
if (tier === TIERS.TIER5) {
- if (!acceptedEulaVersion) {
+ if (!acceptedEulaVersion || !acceptedPrivacyNoticeVersion) {
return false;
}
// 最新バージョンに同意済みか判定
const eulaAccepted = acceptedEulaVersion === latestEulaVersion;
- return eulaAccepted;
+ const privacyNoticeAccepted =
+ acceptedPrivacyNoticeVersion === latestPrivacyNoticeVersion;
+ return eulaAccepted && privacyNoticeAccepted;
} else {
- // 第一~第四階層はEULA、DPAを判定
- if (!acceptedEulaVersion || !acceptedDpaVersion) {
+ // 第一~第四階層はEULA、PrivacyNotice、DPAを判定
+ if (
+ !acceptedEulaVersion ||
+ !acceptedPrivacyNoticeVersion ||
+ !acceptedDpaVersion
+ ) {
return false;
}
// 最新バージョンに同意済みか判定
const eulaAccepted = acceptedEulaVersion === latestEulaVersion;
+ const privacyNoticeAccepted =
+ acceptedPrivacyNoticeVersion === latestPrivacyNoticeVersion;
const dpaAccepted = acceptedDpaVersion === latestDpaVersion;
- return eulaAccepted && dpaAccepted;
+ return eulaAccepted && privacyNoticeAccepted && dpaAccepted;
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
diff --git a/dictation_server/src/features/auth/types/types.ts b/dictation_server/src/features/auth/types/types.ts
index 1be9570..c031508 100644
--- a/dictation_server/src/features/auth/types/types.ts
+++ b/dictation_server/src/features/auth/types/types.ts
@@ -22,8 +22,10 @@ export class AccessTokenRequest {}
export type TermsCheckInfo = {
tier: number;
acceptedEulaVersion?: string;
+ acceptedPrivacyNoticeVersion?: string;
acceptedDpaVersion?: string;
latestEulaVersion: string;
+ latestPrivacyNoticeVersion: string;
latestDpaVersion: string;
};
diff --git a/dictation_server/src/features/files/test/files.service.mock.ts b/dictation_server/src/features/files/test/files.service.mock.ts
index a5b8f06..c69e7bf 100644
--- a/dictation_server/src/features/files/test/files.service.mock.ts
+++ b/dictation_server/src/features/files/test/files.service.mock.ts
@@ -139,6 +139,7 @@ export const makeDefaultUsersRepositoryMockValue =
role: 'none',
author_id: '',
accepted_eula_version: '1.0',
+ accepted_privacy_notice_version: '1.0',
accepted_dpa_version: '1.0',
email_verified: true,
deleted_at: null,
diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts
index cf5aa71..5d5d97c 100644
--- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts
+++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts
@@ -470,6 +470,7 @@ const defaultTasksRepositoryMockValue: {
external_id: 'userId',
role: 'typist',
accepted_eula_version: '',
+ accepted_privacy_notice_version: '',
accepted_dpa_version: '',
email_verified: true,
auto_renew: true,
diff --git a/dictation_server/src/features/terms/terms.service.spec.ts b/dictation_server/src/features/terms/terms.service.spec.ts
index 772e9f5..6ff1176 100644
--- a/dictation_server/src/features/terms/terms.service.spec.ts
+++ b/dictation_server/src/features/terms/terms.service.spec.ts
@@ -34,6 +34,8 @@ describe('利用規約取得', () => {
await createTermInfo(source, 'EULA', 'v1.0');
await createTermInfo(source, 'EULA', 'v1.1');
+ await createTermInfo(source, 'PrivacyNotice', 'v1.0');
+ await createTermInfo(source, 'PrivacyNotice', 'v1.1');
await createTermInfo(source, 'DPA', 'v1.0');
await createTermInfo(source, 'DPA', 'v1.2');
@@ -42,8 +44,10 @@ describe('利用規約取得', () => {
expect(result[0].documentType).toBe('EULA');
expect(result[0].version).toBe('v1.1');
- expect(result[1].documentType).toBe('DPA');
- expect(result[1].version).toBe('v1.2');
+ expect(result[1].documentType).toBe('PrivacyNotice');
+ expect(result[1].version).toBe('v1.1');
+ expect(result[2].documentType).toBe('DPA');
+ expect(result[2].version).toBe('v1.2');
});
it('利用規約情報(EULA、DPA両方)が存在しない場合エラーとなる', async () => {
@@ -75,6 +79,21 @@ describe('利用規約取得', () => {
);
});
+ it('利用規約情報(PrivacyNoticeのみ)が存在しない場合エラーとなる', async () => {
+ if (!source) fail();
+ const module = await makeTestingModule(source);
+ if (!module) fail();
+ const service = module.get(TermsService);
+ await createTermInfo(source, 'PrivacyNotice', 'v1.0');
+ const context = makeContext(uuidv4());
+ await expect(service.getTermsInfo(context)).rejects.toEqual(
+ new HttpException(
+ makeErrorResponse('E009999'),
+ HttpStatus.INTERNAL_SERVER_ERROR,
+ ),
+ );
+ });
+
it('利用規約情報(DPAのみ)が存在しない場合エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
diff --git a/dictation_server/src/features/terms/terms.service.ts b/dictation_server/src/features/terms/terms.service.ts
index 65137a1..ee52415 100644
--- a/dictation_server/src/features/terms/terms.service.ts
+++ b/dictation_server/src/features/terms/terms.service.ts
@@ -19,13 +19,17 @@ export class TermsService {
`[IN] [${context.getTrackingId()}] ${this.getTermsInfo.name}`,
);
try {
- const { eulaVersion, dpaVersion } =
+ const { eulaVersion, privacyNoticeVersion, dpaVersion } =
await this.termsRepository.getLatestTermsInfo();
return [
{
documentType: TERM_TYPE.EULA,
version: eulaVersion,
},
+ {
+ documentType: TERM_TYPE.PRIVACY_NOTICE,
+ version: privacyNoticeVersion,
+ },
{
documentType: TERM_TYPE.DPA,
version: dpaVersion,
diff --git a/dictation_server/src/features/terms/types/types.ts b/dictation_server/src/features/terms/types/types.ts
index 6a45eae..479960e 100644
--- a/dictation_server/src/features/terms/types/types.ts
+++ b/dictation_server/src/features/terms/types/types.ts
@@ -13,5 +13,6 @@ export class GetTermsInfoResponse {
export type TermsVersion = {
eulaVersion: string;
+ privacyNoticeVersion: string;
dpaVersion: string;
};
diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts
index 9f48418..d21f72d 100644
--- a/dictation_server/src/features/users/types/types.ts
+++ b/dictation_server/src/features/users/types/types.ts
@@ -263,6 +263,8 @@ export class UpdateAcceptedVersionRequest {
idToken: string;
@ApiProperty({ description: '更新バージョン(EULA)' })
acceptedEULAVersion: string;
+ @ApiProperty({ description: '更新バージョン(PrivacyNotice)' })
+ acceptedPrivacyNoticeVersion: string;
@ApiProperty({ description: '更新バージョン(DPA)', required: false })
acceptedDPAVersion?: string;
}
diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts
index a92bbd2..c8cdafc 100644
--- a/dictation_server/src/features/users/users.controller.ts
+++ b/dictation_server/src/features/users/users.controller.ts
@@ -4,6 +4,7 @@ import {
Get,
HttpException,
HttpStatus,
+ Ip,
Post,
Query,
Req,
@@ -136,6 +137,7 @@ export class UsersController {
@Get()
async getUsers(@Req() req: Request): Promise {
const accessToken = retrieveAuthorizationToken(req);
+
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
@@ -627,7 +629,12 @@ export class UsersController {
async updateAcceptedVersion(
@Body() body: UpdateAcceptedVersionRequest,
): Promise {
- const { idToken, acceptedEULAVersion, acceptedDPAVersion } = body;
+ const {
+ idToken,
+ acceptedEULAVersion,
+ acceptedPrivacyNoticeVersion,
+ acceptedDPAVersion,
+ } = body;
const context = makeContext(uuidv4());
@@ -650,6 +657,7 @@ export class UsersController {
context,
verifiedIdToken.sub,
acceptedEULAVersion,
+ acceptedPrivacyNoticeVersion,
acceptedDPAVersion,
);
return {};
diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts
index 83894ee..cea6f31 100644
--- a/dictation_server/src/features/users/users.service.spec.ts
+++ b/dictation_server/src/features/users/users.service.spec.ts
@@ -208,6 +208,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
account_id: 1,
role: 'None',
accepted_eula_version: 'string',
+ accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string',
email_verified: false,
created_by: 'string;',
@@ -259,6 +260,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
account_id: 1,
role: 'None',
accepted_eula_version: 'string',
+ accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string',
email_verified: false,
created_by: 'string;',
@@ -306,6 +308,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
account_id: 1,
role: 'None',
accepted_eula_version: 'string',
+ accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string',
email_verified: true,
created_by: 'string;',
@@ -358,6 +361,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
account_id: 1,
role: 'None',
accepted_eula_version: 'string',
+ accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string',
email_verified: false,
created_by: 'string;',
@@ -2617,7 +2621,12 @@ describe('UsersService.updateAcceptedVersion', () => {
const context = makeContext(uuidv4());
const service = module.get(UsersService);
- await service.updateAcceptedVersion(context, admin.external_id, 'v2.0');
+ await service.updateAcceptedVersion(
+ context,
+ admin.external_id,
+ 'v2.0',
+ 'v2.0',
+ );
const user = await getUser(source, admin.id);
expect(user?.accepted_eula_version).toBe('v2.0');
@@ -2637,6 +2646,7 @@ describe('UsersService.updateAcceptedVersion', () => {
context,
admin.external_id,
'v2.0',
+ 'v2.0',
'v3.0',
);
const user = await getUser(source, admin.id);
@@ -2660,6 +2670,7 @@ describe('UsersService.updateAcceptedVersion', () => {
context,
admin.external_id,
'v2.0',
+ 'v2.0',
undefined,
),
).rejects.toEqual(
diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts
index cf82fd5..e26a1c1 100644
--- a/dictation_server/src/features/users/users.service.ts
+++ b/dictation_server/src/features/users/users.service.ts
@@ -403,6 +403,7 @@ export class UsersService {
role,
accepted_dpa_version: null,
accepted_eula_version: null,
+ accepted_privacy_notice_version: null,
encryption: false,
encryption_password: null,
prompt: false,
@@ -422,6 +423,7 @@ export class UsersService {
prompt: prompt ?? false,
accepted_dpa_version: null,
accepted_eula_version: null,
+ accepted_privacy_notice_version: null,
};
default:
//不正なroleが指定された場合はログを出力してエラーを返す
@@ -538,6 +540,7 @@ export class UsersService {
// DBから同一アカウントのユーザ一覧を取得する
const dbUsers = await this.usersRepository.findSameAccountUsers(
externalId,
+ context,
);
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
@@ -1044,12 +1047,14 @@ export class UsersService {
* @param context
* @param idToken
* @param eulaVersion
+ * @param privacyNoticeVersion
* @param dpaVersion
*/
async updateAcceptedVersion(
context: Context,
externalId: string,
eulaVersion: string,
+ privacyNoticeVersion: string,
dpaVersion?: string,
): Promise {
this.logger.log(
@@ -1058,6 +1063,7 @@ export class UsersService {
} | params: { ` +
`externalId: ${externalId}, ` +
`eulaVersion: ${eulaVersion}, ` +
+ `privacyNoticeVersion: ${privacyNoticeVersion}, ` +
`dpaVersion: ${dpaVersion}, };`,
);
@@ -1065,6 +1071,7 @@ export class UsersService {
await this.usersRepository.updateAcceptedTermsVersion(
externalId,
eulaVersion,
+ privacyNoticeVersion,
dpaVersion,
);
} catch (e) {
diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts
index 1df7d13..d7762bd 100644
--- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts
+++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts
@@ -127,6 +127,7 @@ export class AccountsRepositoryService {
adminExternalUserId: string,
adminUserRole: string,
adminUserAcceptedEulaVersion?: string,
+ adminUserAcceptedPrivacyNoticeVersion?: string,
adminUserAcceptedDpaVersion?: string,
): Promise<{ newAccount: Account; adminUser: User }> {
return await this.dataSource.transaction(async (entityManager) => {
@@ -148,6 +149,8 @@ export class AccountsRepositoryService {
user.external_id = adminExternalUserId;
user.role = adminUserRole;
user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null;
+ user.accepted_privacy_notice_version =
+ adminUserAcceptedPrivacyNoticeVersion ?? null;
user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null;
}
const usersRepo = entityManager.getRepository(User);
diff --git a/dictation_server/src/repositories/terms/terms.repository.service.ts b/dictation_server/src/repositories/terms/terms.repository.service.ts
index 7c79f24..7deee0f 100644
--- a/dictation_server/src/repositories/terms/terms.repository.service.ts
+++ b/dictation_server/src/repositories/terms/terms.repository.service.ts
@@ -24,6 +24,14 @@ export class TermsRepositoryService {
id: 'DESC',
},
});
+ const latestPrivacyNoticeInfo = await termRepo.findOne({
+ where: {
+ document_type: TERM_TYPE.PRIVACY_NOTICE,
+ },
+ order: {
+ id: 'DESC',
+ },
+ });
const latestDpaInfo = await termRepo.findOne({
where: {
document_type: TERM_TYPE.DPA,
@@ -33,13 +41,16 @@ export class TermsRepositoryService {
},
});
- if (!latestEulaInfo || !latestDpaInfo) {
+ if (!latestEulaInfo || !latestPrivacyNoticeInfo || !latestDpaInfo) {
throw new TermInfoNotFoundError(
- `Terms info is not found. latestEulaInfo: ${latestEulaInfo}, latestDpaInfo: ${latestDpaInfo}`,
+ `Terms info is not found. latestEulaInfo: ${latestEulaInfo},
+ latestPrivacyNoticeInfo: ${latestPrivacyNoticeInfo},
+ latestDpaInfo: ${latestDpaInfo}`,
);
}
return {
eulaVersion: latestEulaInfo.version,
+ privacyNoticeVersion: latestEulaInfo.version,
dpaVersion: latestDpaInfo.version,
};
});
diff --git a/dictation_server/src/repositories/users/entity/user.entity.ts b/dictation_server/src/repositories/users/entity/user.entity.ts
index 34d0bca..0f4e57c 100644
--- a/dictation_server/src/repositories/users/entity/user.entity.ts
+++ b/dictation_server/src/repositories/users/entity/user.entity.ts
@@ -34,6 +34,9 @@ export class User {
@Column({ nullable: true, type: 'varchar' })
accepted_eula_version: string | null;
+ @Column({ nullable: true, type: 'varchar' })
+ accepted_privacy_notice_version: string | null;
+
@Column({ nullable: true, type: 'varchar' })
accepted_dpa_version: string | null;
@@ -112,6 +115,9 @@ export class UserArchive {
@Column({ nullable: true, type: 'varchar' })
accepted_eula_version: string | null;
+ @Column({ nullable: true, type: 'varchar' })
+ accepted_privacy_notice_version: string | null;
+
@Column({ nullable: true, type: 'varchar' })
accepted_dpa_version: string | null;
diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts
index af179e9..8894656 100644
--- a/dictation_server/src/repositories/users/users.repository.service.ts
+++ b/dictation_server/src/repositories/users/users.repository.service.ts
@@ -35,6 +35,7 @@ import {
import { Account } from '../accounts/entity/account.entity';
import { Workflow } from '../workflows/entity/workflow.entity';
import { Worktype } from '../worktypes/entity/worktype.entity';
+import { Context } from '../../common/log';
@Injectable()
export class UsersRepositoryService {
@@ -340,7 +341,10 @@ export class UsersRepositoryService {
* @param externalId
* @returns User[]
*/
- async findSameAccountUsers(external_id: string): Promise {
+ async findSameAccountUsers(
+ external_id: string,
+ context: Context,
+ ): Promise {
return await this.dataSource.transaction(async (entityManager) => {
const repo = entityManager.getRepository(User);
@@ -359,8 +363,9 @@ export class UsersRepositoryService {
license: true,
},
where: { account_id: accountId },
+ comment: `${context.getTrackingId()}`,
});
-
+
return dbUsers;
});
}
@@ -471,6 +476,14 @@ export class UsersRepositoryService {
id: 'DESC',
},
});
+ const latestPrivacyNoticeInfo = await termRepo.findOne({
+ where: {
+ document_type: TERM_TYPE.PRIVACY_NOTICE,
+ },
+ order: {
+ id: 'DESC',
+ },
+ });
const latestDpaInfo = await termRepo.findOne({
where: {
document_type: TERM_TYPE.DPA,
@@ -479,16 +492,18 @@ export class UsersRepositoryService {
id: 'DESC',
},
});
-
- if (!latestEulaInfo || !latestDpaInfo) {
+ if (!latestEulaInfo || !latestPrivacyNoticeInfo || !latestDpaInfo) {
throw new TermInfoNotFoundError(`Terms info is not found.`);
}
return {
tier: user.account.tier,
acceptedEulaVersion: user.accepted_eula_version ?? undefined,
+ acceptedPrivacyNoticeVersion:
+ user.accepted_privacy_notice_version ?? undefined,
acceptedDpaVersion: user.accepted_dpa_version ?? undefined,
latestEulaVersion: latestEulaInfo.version,
+ latestPrivacyNoticeVersion: latestPrivacyNoticeInfo.version,
latestDpaVersion: latestDpaInfo.version,
};
});
@@ -498,12 +513,14 @@ export class UsersRepositoryService {
* 同意済み利用規約のバージョンを更新する
* @param externalId
* @param eulaVersion
+ * @param privacyNoticeVersion
* @param dpaVersion
* @returns update
*/
async updateAcceptedTermsVersion(
externalId: string,
eulaVersion: string,
+ privacyNoticeVersion: string,
dpaVersion: string | undefined,
): Promise {
await this.dataSource.transaction(async (entityManager) => {
@@ -531,6 +548,11 @@ export class UsersRepositoryService {
if (!eulaVersion) {
throw new UpdateTermsVersionNotSetError(`EULA version param not set.`);
}
+ if (!privacyNoticeVersion) {
+ throw new UpdateTermsVersionNotSetError(
+ `PrivacyNotice version param not set.`,
+ );
+ }
if (user.account.tier !== TIERS.TIER5 && !dpaVersion) {
throw new UpdateTermsVersionNotSetError(
`DPA version param not set. User's tier: ${user.account.tier}`,
@@ -538,6 +560,8 @@ export class UsersRepositoryService {
}
user.accepted_eula_version = eulaVersion;
+ user.accepted_privacy_notice_version =
+ privacyNoticeVersion ?? user.accepted_privacy_notice_version;
user.accepted_dpa_version = dpaVersion ?? user.accepted_dpa_version;
await userRepo.update({ id: user.id }, user);
});