Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions apps/app/src/server/routes/apiv3/forgot-password.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import PasswordResetOrder from '~/server/models/password-reset-order';
import { configManager } from '~/server/service/config-manager';
import { growiInfoService } from '~/server/service/growi-info';
import loggerFactory from '~/utils/logger';
import { resolveLocaleTemplatePath } from '~/server/util/locale-utils';

import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
import httpErrorHandler from '../../middlewares/http-error-handler';
Expand Down Expand Up @@ -45,7 +46,6 @@ const router = express.Router();
module.exports = (crowi) => {
const { appService, mailService } = crowi;
const User = crowi.model('User');
const path = require('path');

const addActivity = generateAddActivityMiddleware(crowi);

Expand Down Expand Up @@ -77,10 +77,16 @@ module.exports = (crowi) => {
const checkPassportStrategyMiddleware = checkForgotPasswordEnabledMiddlewareFactory(crowi, true);

async function sendPasswordResetEmail(templateFileName, locale, email, url, expiredAt) {
const templatePath = await resolveLocaleTemplatePath({
baseDir: crowi.localeDir,
locale,
templateSegments: ['notifications', `${templateFileName}.ejs`],
});

return mailService.send({
to: email,
subject: '[GROWI] Password Reset',
template: path.join(crowi.localeDir, `${locale}/notifications/${templateFileName}.ejs`),
template: templatePath,
vars: {
appTitle: appService.getAppTitle(),
email,
Expand Down Expand Up @@ -148,7 +154,8 @@ module.exports = (crowi) => {
catch (err) {
const msg = 'Error occurred during password reset request procedure.';
logger.error(err);
return res.apiv3Err(`${msg} Cause: ${err}`);
// Keep response generic so internal error details are not exposed to clients.
return res.apiv3Err(msg);
}
});

Expand Down
15 changes: 11 additions & 4 deletions apps/app/src/server/routes/apiv3/user-activation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import path from 'path';

import type { IUser } from '@growi/core';
import { ErrorV3 } from '@growi/core/dist/models';
import { format, subSeconds } from 'date-fns';
Expand All @@ -14,6 +12,7 @@ import { configManager } from '~/server/service/config-manager';
import { growiInfoService } from '~/server/service/growi-info';
import { getTranslation } from '~/server/service/i18next';
import loggerFactory from '~/utils/logger';
import { resolveLocaleTemplatePath } from '~/server/util/locale-utils';

const logger = loggerFactory('growi:routes:apiv3:user-activation');

Expand Down Expand Up @@ -188,7 +187,11 @@ export const completeRegistrationAction = (crowi: Crowi) => {
const admins = await User.findAdmins();
const appTitle = appService.getAppTitle();
const locale = configManager.getConfig('app:globalLang');
const template = path.join(crowi.localeDir, `${locale}/admin/userWaitingActivation.ejs`);
const template = await resolveLocaleTemplatePath({
baseDir: crowi.localeDir,
locale,
templateSegments: ['admin', 'userWaitingActivation.ejs'],
});
const url = growiInfoService.getSiteUrl();

sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url);
Expand Down Expand Up @@ -274,7 +277,11 @@ async function makeRegistrationEmailToken(email, crowi: Crowi) {
return mailService.send({
to: email,
subject: '[GROWI] User Activation',
template: path.join(localeDir, `${locale}/notifications/userActivation.ejs`),
template: await resolveLocaleTemplatePath({
baseDir: localeDir,
locale,
templateSegments: ['notifications', 'userActivation.ejs'],
}),
vars: {
appTitle: appService.getAppTitle(),
email,
Expand Down
17 changes: 13 additions & 4 deletions apps/app/src/server/routes/apiv3/users.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import path from 'path';

import { SCOPE } from '@growi/core/dist/interfaces';
import { ErrorV3 } from '@growi/core/dist/models';
import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
Expand All @@ -21,6 +19,7 @@ import { configManager } from '~/server/service/config-manager';
import { growiInfoService } from '~/server/service/growi-info';
import { deleteCompletelyUserHomeBySystem } from '~/server/service/page/delete-completely-user-home-by-system';
import loggerFactory from '~/utils/logger';
import { resolveLocaleTemplatePath } from '~/server/util/locale-utils';

import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
Expand Down Expand Up @@ -183,14 +182,19 @@ module.exports = (crowi) => {
const appTitle = appService.getAppTitle();
const locale = configManager.getConfig('app:globalLang');
const failedToSendEmailList = [];
const templatePath = await resolveLocaleTemplatePath({
baseDir: crowi.localeDir,
locale,
templateSegments: ['admin', 'userInvitation.ejs'],
});

for (const user of userList) {
try {
// eslint-disable-next-line no-await-in-loop
await mailService.send({
to: user.email,
subject: `Invitation to ${appTitle}`,
template: path.join(crowi.localeDir, `${locale}/admin/userInvitation.ejs`),
template: templatePath,
vars: {
email: user.email,
password: user.password,
Expand All @@ -217,11 +221,16 @@ module.exports = (crowi) => {
const { appService, mailService } = crowi;
const appTitle = appService.getAppTitle();
const locale = configManager.getConfig('app:globalLang');
const templatePath = await resolveLocaleTemplatePath({
baseDir: crowi.localeDir,
locale,
templateSegments: ['admin', 'userResetPassword.ejs'],
});

await mailService.send({
to: user.email,
subject: `New password for ${appTitle}`,
template: path.join(crowi.localeDir, `${locale}/admin/userResetPassword.ejs`),
template: templatePath,
vars: {
email: user.email,
password: user.password,
Expand Down
9 changes: 7 additions & 2 deletions apps/app/src/server/routes/login.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
import { configManager } from '~/server/service/config-manager';
import loggerFactory from '~/utils/logger';
import { resolveLocaleTemplatePath } from '~/server/util/locale-utils';

import { growiInfoService } from '../service/growi-info';

Expand All @@ -10,7 +11,6 @@ import { growiInfoService } from '../service/growi-info';
/** @param {import('~/server/crowi').default} crowi Crowi instance */
module.exports = function(crowi, app) {
const logger = loggerFactory('growi:routes:login');
const path = require('path');
const User = crowi.model('User');
const {
appService, aclService, mailService, activityService,
Expand All @@ -24,12 +24,17 @@ module.exports = function(crowi, app) {
const admins = await User.findAdmins();
const appTitle = appService.getAppTitle();
const locale = configManager.getConfig('app:globalLang');
const templatePath = await resolveLocaleTemplatePath({
baseDir: crowi.localeDir,
locale,
templateSegments: ['admin', 'userWaitingActivation.ejs'],
});

const promises = admins.map((admin) => {
return mailService.send({
to: admin.email,
subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
template: path.join(crowi.localeDir, `${locale}/admin/userWaitingActivation.ejs`),
template: templatePath,
vars: {
adminUser: admin,
createdUser: userData,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
import type { ConfigDefinition, Lang, NonBlankString } from '@growi/core/dist/interfaces';
import type { ConfigDefinition, NonBlankString } from '@growi/core/dist/interfaces';
import {
Lang,
toNonBlankString,
defineConfig,
} from '@growi/core/dist/interfaces';
Expand Down Expand Up @@ -389,8 +390,8 @@ export const CONFIG_DEFINITIONS = {
'app:timezone': defineConfig<number | undefined>({
defaultValue: undefined,
}),
'app:globalLang': defineConfig<string>({
defaultValue: 'en_US',
'app:globalLang': defineConfig<Lang>({
defaultValue: Lang.en_US,
}),
'app:fileUpload': defineConfig<boolean>({
defaultValue: false,
Expand Down
49 changes: 35 additions & 14 deletions apps/app/src/server/service/config-manager/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,46 @@ import { toBoolean } from '@growi/core/dist/utils/env-utils';

import loggerFactory from '~/utils/logger';

import { Lang } from '@growi/core/dist/interfaces';
import { coerceToSupportedLang } from '../../util/locale-utils';

import type { ConfigKey, ConfigValues } from './config-definition';
import { CONFIG_DEFINITIONS } from './config-definition';

const logger = loggerFactory('growi:service:ConfigLoader');

export const sanitizeConfigValue = (key: ConfigKey, value: unknown): ConfigValues[ConfigKey] => {
switch (key) {
case 'app:globalLang':
return coerceToSupportedLang(value, { fallback: Lang.en_US }) as ConfigValues[ConfigKey];
case 'autoInstall:globalLang':
return coerceToSupportedLang(value, { allowUndefined: true }) as ConfigValues[ConfigKey];
default:
return value as ConfigValues[ConfigKey];
}
};

export class ConfigLoader implements IConfigLoader<ConfigKey, ConfigValues> {

async loadFromEnv(): Promise<RawConfigData<ConfigKey, ConfigValues>> {
const envConfig = {} as RawConfigData<ConfigKey, ConfigValues>;

for (const [key, metadata] of Object.entries(CONFIG_DEFINITIONS)) {
let configValue = metadata.defaultValue;
let configValue: unknown = metadata.defaultValue;

if (metadata.envVarName != null) {
const envVarValue = process.env[metadata.envVarName];
if (envVarValue != null) {
configValue = this.parseEnvValue(envVarValue, typeof metadata.defaultValue) as ConfigValues[ConfigKey];
configValue = this.parseEnvValue(envVarValue, typeof metadata.defaultValue);
}
}

envConfig[key as ConfigKey] = {
const typedKey = key as ConfigKey;
const sanitizedValue = sanitizeConfigValue(typedKey, configValue);

envConfig[typedKey] = {
definition: metadata,
value: configValue,
value: sanitizedValue,
};
}

Expand All @@ -42,16 +59,20 @@ export class ConfigLoader implements IConfigLoader<ConfigKey, ConfigValues> {
const docs = await Config.find().exec();

for (const doc of docs) {
dbConfig[doc.key as ConfigKey] = {
definition: (doc.key in CONFIG_DEFINITIONS) ? CONFIG_DEFINITIONS[doc.key as ConfigKey] : undefined,
value: doc.value != null ? (() => {
try {
return JSON.parse(doc.value);
}
catch {
return null;
}
})() : null,
const typedKey = doc.key as ConfigKey;
const parsedValue: unknown = doc.value != null ? (() => {
try {
return JSON.parse(doc.value);
}
catch {
return null;
}
})() : null;
const sanitizedValue = sanitizeConfigValue(typedKey, parsedValue);

dbConfig[typedKey] = {
definition: (doc.key in CONFIG_DEFINITIONS) ? CONFIG_DEFINITIONS[typedKey] : undefined,
value: sanitizedValue,
};
}

Expand Down
23 changes: 15 additions & 8 deletions apps/app/src/server/service/config-manager/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { S2sMessageHandlable } from '../s2s-messaging/handlable';

import type { ConfigKey, ConfigValues } from './config-definition';
import { ENV_ONLY_GROUPS } from './config-definition';
import { ConfigLoader } from './config-loader';
import { ConfigLoader, sanitizeConfigValue } from './config-loader';

const logger = loggerFactory('growi:service:ConfigManager');

Expand Down Expand Up @@ -111,14 +111,18 @@ export class ConfigManager implements IConfigManagerForApp, S2sMessageHandlable
// Dynamic import to avoid loading database modules too early
const { Config } = await import('../../models/config');

if (options?.removeIfUndefined && value === undefined) {
// remove the config if the value is undefined and removeIfUndefined is true
const sanitizedValue = value === undefined ? undefined : sanitizeConfigValue(key, value);
const shouldRemove = sanitizedValue === undefined
&& (value === undefined ? (options?.removeIfUndefined ?? false) : true);

if (shouldRemove) {
// remove the config when sanitized value is undefined
await Config.deleteOne({ key });
}
else {
await Config.updateOne(
{ key },
{ value: JSON.stringify(value) },
{ value: JSON.stringify(sanitizedValue) },
{ upsert: true },
);
}
Expand All @@ -135,14 +139,17 @@ export class ConfigManager implements IConfigManagerForApp, S2sMessageHandlable
const { Config } = await import('../../models/config');

const operations = Object.entries(updates).map(([key, value]) => {
return (options?.removeIfUndefined && value === undefined)
// remove the config if the value is undefined
const typedKey = key as ConfigKey;
const sanitizedValue = value === undefined ? undefined : sanitizeConfigValue(typedKey, value);
const shouldRemove = sanitizedValue === undefined
&& (value === undefined ? (options?.removeIfUndefined ?? false) : true);

return shouldRemove
? { deleteOne: { filter: { key } } }
// update
: {
updateOne: {
filter: { key },
update: { value: JSON.stringify(value) },
update: { value: JSON.stringify(sanitizedValue) },
upsert: true,
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import nodePath from 'path';

import { GlobalNotificationSettingEvent, GlobalNotificationSettingType } from '~/server/models/GlobalNotificationSetting';
import { configManager } from '~/server/service/config-manager';
import { growiInfoService } from '~/server/service/growi-info';
import loggerFactory from '~/utils/logger';
import { resolveLocaleTemplatePath } from '~/server/util/locale-utils';

const logger = loggerFactory('growi:service:GlobalNotificationMailService'); // eslint-disable-line no-unused-vars

Expand Down Expand Up @@ -36,7 +35,7 @@ class GlobalNotificationMailService {
const GlobalNotification = this.crowi.model('GlobalNotificationSetting');
const notifications = await GlobalNotification.findSettingByPathAndEvent(event, page.path, GlobalNotificationSettingType.MAIL);

const option = this.generateOption(event, page, triggeredBy, vars);
const option = await this.generateOption(event, page, triggeredBy, vars);

await Promise.all(notifications.map((notification) => {
return mailService.send({ ...option, to: notification.toEmail });
Expand All @@ -55,14 +54,18 @@ class GlobalNotificationMailService {
*
* @return {{ subject: string, template: string, vars: object }}
*/
generateOption(event, page, triggeredBy, { comment, oldPath }) {
async generateOption(event, page, triggeredBy, { comment, oldPath }) {
const locale = configManager.getConfig('app:globalLang');
// validate for all events
if (event == null || page == null || triggeredBy == null) {
throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
}

const template = nodePath.join(this.crowi.localeDir, `${locale}/notifications/${event}.ejs`);
const template = await resolveLocaleTemplatePath({
baseDir: this.crowi.localeDir,
locale,
templateSegments: ['notifications', `${event}.ejs`],
});

const path = page.path;
const appTitle = this.crowi.appService.getAppTitle();
Expand Down
Loading
Loading