Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
dc781a0
Define new user page visibility setting in config definitions
arvid-e Nov 27, 2025
2e2c179
Add hide user page setting to general setting api
arvid-e Nov 27, 2025
5578871
Rename user page visibility setting
arvid-e Dec 1, 2025
9d144bc
Add user page visibility setting to general admin settings
arvid-e Dec 9, 2025
1a89579
Add user page visibility setting to general security settings update …
arvid-e Dec 9, 2025
2447c16
Create react component for user page visibility settings
arvid-e Dec 9, 2025
09bfcf2
Change setting variable name
arvid-e Dec 10, 2025
39d39ce
Update setting description
arvid-e Dec 10, 2025
1bf5d67
Write translations for user page visibility setting
arvid-e Dec 15, 2025
9af79e1
fix ja_JP/admin.json
yuki-takei Dec 15, 2025
a591dbe
Change name of page visibility boolean
arvid-e Dec 16, 2025
3fb5d84
Change to backend solution
arvid-e Dec 23, 2025
aee1fe5
Move user page hiding implementation to PageListingService
arvid-e Dec 24, 2025
376a801
Hide user content form lsx command
arvid-e Dec 24, 2025
f4cfa84
Add reminder
arvid-e Dec 24, 2025
3927130
Change to correct config name
arvid-e Dec 25, 2025
4f79de2
Update listPages to take user page hiding setting
arvid-e Dec 25, 2025
3c562dc
Update test to work with changes to listPages
arvid-e Dec 25, 2025
eece6e4
Include configManager mock in test
arvid-e Dec 25, 2025
9603929
Use list of excluded paths to not be usable using lsx
arvid-e Jan 5, 2026
7adb254
Change test to work with crowi argument
arvid-e Jan 5, 2026
2e2ef01
Update test to take new pageService method into account
arvid-e Jan 5, 2026
a785441
Create tests for empty and providede excludedPaths
arvid-e Jan 7, 2026
5c312c3
Hide user content from general users in recent changes tab
arvid-e Dec 24, 2025
45735ce
Show pages that happen to start with user but are not user pages
arvid-e Dec 25, 2025
aabc628
Hide user pages in recent changes for admin also
arvid-e Jan 7, 2026
ec37787
Fix missed conflict handling
arvid-e Jan 13, 2026
6070841
Handle query being undefined
arvid-e Jan 13, 2026
03d105e
Format code
arvid-e Jan 13, 2026
04ac8e6
Fix list pages test problem
arvid-e Jan 13, 2026
efb23f5
Throw 400 if pagepath is undefined
arvid-e Jan 13, 2026
8b809dc
Format code
arvid-e Jan 13, 2026
5b41279
Format code
arvid-e Jan 13, 2026
dbb3828
Set text description in user page setting more safely
arvid-e Jan 13, 2026
8eb58b1
Remove redundant null check for pagePath
arvid-e Jan 13, 2026
8fa90b2
Throw 400 if pagepath is empty string
arvid-e Jan 13, 2026
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
5 changes: 5 additions & 0 deletions apps/app/public/static/locales/en_US/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
"enable_force_delete_user_homepage_on_user_deletion": "When you delete a user, the user's homepage and all its sub pages will be completely deleted",
"desc": "You will be able to delete a deleted user's homepage."
},
"user_page_visibility": {
"user_page_visibility": "User page visibility",
"hide_user_pages": "Hide user pages",
"desc": "Hides all user related pages for general users"
},
"session": "Session",
"max_age": "Max age (msec)",
"max_age_desc": "Specifies the number (in milliseconds) to expire users session.<br>Default: 2592000000 (30days)",
Expand Down
5 changes: 5 additions & 0 deletions apps/app/public/static/locales/fr_FR/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
"enable_force_delete_user_homepage_on_user_deletion": "Supprimer la page d'accueil et ses pages enfants",
"desc": "Les pages d'accueil utilisateurs pourront être supprimées."
},
"user_page_visibility": {
"user_page_visibility": "Visibilité de la page utilisateur",
"hide_user_pages": "Masquer les pages utilisateur",
"desc": "Masque toutes les pages liées aux utilisateurs pour les utilisateurs généraux"
},
"session": "Session",
"max_age": "Âge maximal (ms)",
"max_age_desc": "Spécifie (en milliseconde) l'âge maximal d'une session <br>Par défaut: 2592000000 (30 jours)",
Expand Down
5 changes: 5 additions & 0 deletions apps/app/public/static/locales/ja_JP/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
"enable_force_delete_user_homepage_on_user_deletion": "ユーザーを削除したとき、ユーザーホームページとその配下のページを完全削除する",
"desc": "削除済みユーザーのユーザーホームページを削除できるようになります。"
},
"user_page_visibility": {
"user_page_visibility": "ユーザーページの表示/非表示",
"hide_user_pages": "ユーザーページを非表示にする",
"desc": "一般ユーザーに対して、すべてのユーザー関連ページを非表示にする"
},
"session": "セッション",
"max_age": "有効期間 (ミリ秒)",
"max_age_desc": "ユーザーのセッション情報の有効期間をミリ秒で指定できます。<br>デフォルト値: 2592000000 (30日間)",
Expand Down
5 changes: 5 additions & 0 deletions apps/app/public/static/locales/ko_KR/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
"enable_force_delete_user_homepage_on_user_deletion": "사용자를 삭제할 때, 사용자의 홈페이지와 모든 하위 페이지가 완전히 삭제됩니다.",
"desc": "삭제된 사용자의 홈페이지를 삭제할 수 있습니다."
},
"user_page_visibility": {
"user_page_visibility": "사용자 페이지 가시성",
"hide_user_pages": "사용자 페이지 숨기기",
"desc": "일반 사용자를 위해 모든 사용자 관련 페이지를 숨깁니다"
},
"session": "세션",
"max_age": "최대 수명 (밀리초)",
"max_age_desc": "사용자 세션이 만료되는 시간(밀리초)을 지정합니다.<br>기본값: 2592000000 (30일)",
Expand Down
5 changes: 5 additions & 0 deletions apps/app/public/static/locales/zh_CN/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
"enable_force_delete_user_homepage_on_user_deletion": "删除用户时,该用户的主页及其所有子页面将被完全删除",
"desc": "您可以删除已删除用户的主页。"
},
"user_page_visibility": {
"user_page_visibility": "用户页面可见性",
"hide_user_pages": "隐藏用户页面",
"desc": "对一般用户隐藏所有用户相关页面"
},
"session": "会议",
"max_age": "有效期间 (msec)",
"max_age_desc": "指定使用户会话过期的数量(以毫秒为单位)。<br>默认值: 2592000000 (30天)",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable react/no-danger */
import type React from 'react';

import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';

type Props = {
adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
t: (key: string) => string;
};

export const UserPageVisibilitySettings: React.FC<Props> = ({
adminGeneralSecurityContainer,
t,
}) => {
return (
<>
<h4 className="mb-3">
{t('security_settings.user_page_visibility.user_page_visibility')}
</h4>
<div className="row mb-4">
<div className="col-md-10 offset-md-2">
<div className="form-check form-switch form-check-success">
<input
type="checkbox"
className="form-check-input"
id="is-user-pages-visible"
checked={adminGeneralSecurityContainer.state.isHidingUserPages}
onChange={() => {
adminGeneralSecurityContainer.changeUserPageVisibility();
}}
/>
<label
className="form-label form-check-label"
htmlFor="is-user-pages-visible"
>
{t('security_settings.user_page_visibility.hide_user_pages')}
</label>
</div>
<p className="form-text text-muted small mt-2">
{t('security_settings.user_page_visibility.desc')}
</p>
</div>
</div>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PageDeleteRightsSettings } from './PageDeleteRightsSettings';
import { PageListDisplaySettings } from './PageListDisplaySettings';
import { SessionMaxAgeSettings } from './SessionMaxAgeSettings';
import { UserHomepageDeletionSettings } from './UserHomepageDeletionSettings';
import { UserPageVisibilitySettings } from './UserPageVisibilitySettings';

type FormData = {
sessionMaxAge: string;
Expand Down Expand Up @@ -63,6 +64,8 @@ const SecuritySettingComponent: React.FC<Props> = ({
hideRestrictedByOwner:
adminGeneralSecurityContainer.state
.currentOwnerRestrictionDisplayMode === 'Hidden',
isHidingUserPages:
adminGeneralSecurityContainer.state.isHidingUserPages,
isUsersHomepageDeletionEnabled:
adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled,
isForceDeleteUserHomepageOnUserDeletion:
Expand Down Expand Up @@ -114,6 +117,10 @@ const SecuritySettingComponent: React.FC<Props> = ({
adminGeneralSecurityContainer={adminGeneralSecurityContainer}
t={t}
/>
<UserPageVisibilitySettings
adminGeneralSecurityContainer={adminGeneralSecurityContainer}
t={t}
/>
<CommentManageRightsSettings
adminGeneralSecurityContainer={adminGeneralSecurityContainer}
t={t}
Expand Down
9 changes: 9 additions & 0 deletions apps/app/src/client/services/AdminGeneralSecurityContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default class AdminGeneralSecurityContainer extends Container {
expandOtherOptionsForCompleteDeletion: false,
isShowRestrictedByOwner: false,
isUsersHomepageDeletionEnabled: false,
isHidingUserPages: false,
isForceDeleteUserHomepageOnUserDeletion: false,
isRomUserAllowedToComment: false,
isLocalEnabled: false,
Expand All @@ -67,6 +68,7 @@ export default class AdminGeneralSecurityContainer extends Container {
this.changeGroupRestrictionDisplayMode.bind(this);
this.changePageDeletionAuthority =
this.changePageDeletionAuthority.bind(this);
this.changeUserPageVisibility = this.changeUserPageVisibility.bind(this);
this.changePageCompleteDeletionAuthority =
this.changePageCompleteDeletionAuthority.bind(this);
this.changePageRecursiveDeletionAuthority =
Expand Down Expand Up @@ -105,6 +107,7 @@ export default class AdminGeneralSecurityContainer extends Container {
isForceDeleteUserHomepageOnUserDeletion:
generalSetting.isForceDeleteUserHomepageOnUserDeletion,
isRomUserAllowedToComment: generalSetting.isRomUserAllowedToComment,
isHidingUserPages: generalSetting.isHidingUserPages,
sessionMaxAge: generalSetting.sessionMaxAge,
wikiMode: generalSetting.wikiMode,
disableLinkSharing: shareLinkSetting.disableLinkSharing,
Expand Down Expand Up @@ -176,6 +179,10 @@ export default class AdminGeneralSecurityContainer extends Container {
this.setState({ currentPageDeletionAuthority: val });
}

changeUserPageVisibility() {
this.setState({ isHidingUserPages: !this.state.isHidingUserPages });
}

/**
* Change pageCompleteDeletionAuthority
*/
Expand Down Expand Up @@ -284,6 +291,7 @@ export default class AdminGeneralSecurityContainer extends Container {
formData.isAllGroupMembershipRequiredForPageCompleteDeletion,
hideRestrictedByGroup: formData.hideRestrictedByGroup,
hideRestrictedByOwner: formData.hideRestrictedByOwner,
isHidingUserPages: formData.isHidingUserPages,
isUsersHomepageDeletionEnabled:
formData.isUsersHomepageDeletionEnabled,
isForceDeleteUserHomepageOnUserDeletion:
Expand All @@ -306,6 +314,7 @@ export default class AdminGeneralSecurityContainer extends Container {
this.state.currentGroupRestrictionDisplayMode === 'Hidden',
hideRestrictedByOwner:
this.state.currentOwnerRestrictionDisplayMode === 'Hidden',
isUserPagesVisible: this.state.isUserPagesVisible,
isUsersHomepageDeletionEnabled:
this.state.isUsersHomepageDeletionEnabled,
isForceDeleteUserHomepageOnUserDeletion:
Expand Down
21 changes: 21 additions & 0 deletions apps/app/src/server/models/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type FindRecentUpdatedPagesOption = {
desc: number;
hideRestrictedByOwner: boolean;
hideRestrictedByGroup: boolean;
hideUserPages: boolean;
};

export type CreateMethod = (
Expand Down Expand Up @@ -430,6 +431,22 @@ export class PageQueryBuilder {
return this;
}

addConditionToListByNotMatchPathAndChildren(str: string): PageQueryBuilder {
const path = normalizePath(str);

if (isTopPage(path)) {
return this;
}

const startsPattern = escapeStringRegexp(path);

this.query = this.query.and({
path: { $not: new RegExp(`^${startsPattern}(/|$)`) },
});

return this;
}

addConditionToListByMatch(str: string): PageQueryBuilder {
// No request is set for "/"
if (str === '/') {
Expand Down Expand Up @@ -920,6 +937,10 @@ schema.statics.findRecentUpdatedPages = async function (
const baseQuery = this.find({});
const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);

if (options.hideUserPages) {
queryBuilder.addConditionToListByNotMatchPathAndChildren('/user');
}

if (!options.includeTrashed) {
queryBuilder.addConditionToExcludeTrashed();
}
Expand Down
11 changes: 10 additions & 1 deletion apps/app/src/server/routes/apiv3/page-listing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,24 @@ const routerFactory = (crowi: Crowi): Router => {
const hideRestrictedByGroup = await configManager.getConfig(
'security:list-policy:hideRestrictedByGroup',
);
const hideUserPages = await configManager.getConfig(
'security:isHidingUserPages',
);

try {
const pages =
let pages =
await pageListingService.findChildrenByParentPathOrIdAndViewer(
(id || path) as string,
req.user,
!hideRestrictedByOwner,
!hideRestrictedByGroup,
);

if (hideUserPages === true) {
const isUserPagePath = /^\/user(\/|$)/;
pages = pages.filter((page) => !isUserPagePath.test(page.path));
}

return res.apiv3({ children: pages });
} catch (err) {
logger.error('Error occurred while finding children.', err);
Expand Down
4 changes: 4 additions & 0 deletions apps/app/src/server/routes/apiv3/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ module.exports = (crowi) => {
const hideRestrictedByGroup = configManager.getConfig(
'security:list-policy:hideRestrictedByGroup',
);
const hideUserPages = configManager.getConfig(
'security:isHidingUserPages',
);

/**
* @type {import('~/server/models/page').FindRecentUpdatedPagesOption}
Expand All @@ -207,6 +210,7 @@ module.exports = (crowi) => {
desc: -1,
hideRestrictedByOwner,
hideRestrictedByGroup,
hideUserPages,
};

try {
Expand Down
13 changes: 13 additions & 0 deletions apps/app/src/server/routes/apiv3/security-settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const validator = {
body('hideRestrictedByGroup')
.if((value) => value != null)
.isBoolean(),
body('isHidingUserPages')
.if((value) => value != null)
.isBoolean(),
body('isUsersHomepageDeletionEnabled')
.if((value) => value != null)
.isBoolean(),
Expand Down Expand Up @@ -217,6 +220,9 @@ const validator = {
* pageCompleteDeletionAuthority:
* type: string
* description: type of pageDeletionAuthority
* isHidingUserPages:
* type: boolean
* description: hide all user pages from general users
* hideRestrictedByOwner:
* type: boolean
* description: enable hide by owner
Expand Down Expand Up @@ -505,6 +511,9 @@ module.exports = (crowi) => {
hideRestrictedByGroup: await configManager.getConfig(
'security:list-policy:hideRestrictedByGroup',
),
isHidingUserPages: await configManager.getConfig(
'security:isHidingUserPages',
),
isUsersHomepageDeletionEnabled: await configManager.getConfig(
'security:user-homepage-deletion:isEnabled',
),
Expand Down Expand Up @@ -995,6 +1004,7 @@ module.exports = (crowi) => {
req.body.hideRestrictedByOwner,
'security:list-policy:hideRestrictedByGroup':
req.body.hideRestrictedByGroup,
'security:isHidingUserPages': req.body.isHidingUserPages,
'security:user-homepage-deletion:isEnabled':
req.body.isUsersHomepageDeletionEnabled,
// Validate user-homepage-deletion config
Expand Down Expand Up @@ -1067,6 +1077,9 @@ module.exports = (crowi) => {
hideRestrictedByGroup: await configManager.getConfig(
'security:list-policy:hideRestrictedByGroup',
),
isHidingUserPages: await configManager.getConfig(
'security:isHidingUserPages',
),
isUsersHomepageDeletionEnabled: await configManager.getConfig(
'security:user-homepage-deletion:isEnabled',
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export const CONFIG_KEYS = [
'security:pageRecursiveDeletionAuthority',
'security:pageRecursiveCompleteDeletionAuthority',
'security:isAllGroupMembershipRequiredForPageCompleteDeletion',
'security:isHidingUserPages',
'security:user-homepage-deletion:isEnabled',
'security:user-homepage-deletion:isForceDeleteUserHomepageOnUserDeletion',
'security:isRomUserAllowedToComment',
Expand Down Expand Up @@ -678,6 +679,9 @@ export const CONFIG_DEFINITIONS = {
defineConfig<boolean>({
defaultValue: true,
}),
'security:isHidingUserPages': defineConfig<boolean>({
defaultValue: false,
}),
'security:user-homepage-deletion:isEnabled': defineConfig<boolean>({
defaultValue: false,
}),
Expand Down
1 change: 1 addition & 0 deletions apps/app/src/server/service/page-listing/page-listing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class PageListingService implements IPageListingService {
user?: IUser,
showPagesRestrictedByOwner = false,
showPagesRestrictedByGroup = false,
hideUserPages = false,
): Promise<IPageForTreeItem[]> {
const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
'Page',
Expand Down
10 changes: 10 additions & 0 deletions apps/app/src/server/service/page/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,16 @@ class PageService implements IPageService {
return renamedPage;
}

getExcludedPathsBySystem(): string[] {
const excludedPaths: string[] = [];

if (configManager.getConfig('security:isHidingUserPages')) {
excludedPaths.push('/user');
}

return excludedPaths;
}

async renameSubOperation(
page,
newPagePathSanitized: string,
Expand Down
6 changes: 6 additions & 0 deletions packages/remark-lsx/src/client/stores/lsx/lsx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ describe('useSWRxLsx integration tests', () => {
const mockCrowi = {
require: () => () => (req: any, res: any, next: any) => next(),
accessTokenParser: () => (req: any, res: any, next: any) => next(),
pageService: {
getExcludedPathsBySystem: vi.fn().mockReturnValue(['/user']),
},
configManager: {
getConfig: vi.fn().mockReturnValue(false),
},
};

// Import and setup the LSX middleware
Expand Down
3 changes: 2 additions & 1 deletion packages/remark-lsx/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@ const middleware = (crowi: any, app: any): void => {
loginRequiredFallback,
);
const accessTokenParser = crowi.accessTokenParser;
const excludedPaths = crowi.pageService.getExcludedPathsBySystem();

app.get(
'/_api/lsx',
accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
loginRequired,
lsxValidator,
paramValidator,
listPages,
listPages({ excludedPaths }),
);
};

Expand Down
Loading
Loading