Move security section

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2026-04-23 11:16:53 +07:00
parent fa0931a157
commit 7dba83771b
19 changed files with 266 additions and 9 deletions
+1 -1
View File
@@ -217,7 +217,7 @@ export function createModel (builder: Builder): void {
name: 'security',
label: setting.string.Security,
icon: setting.icon.Password,
component: setting.component.TwoFactorSettings,
component: setting.component.SecuritySettings,
group: 'settings-account',
role: AccountRole.Guest,
order: 1200
+1
View File
@@ -227,6 +227,7 @@
"SpaceMembersOnly": "Pouze členové prostoru",
"Reset": "Resetovat",
"Security": "Zabezpečení",
"SecurityTabSessions": "Historie relací",
"TwoFactorAuth": "Dvoufaktorové ověřování",
"TwoFactorAuthDescription": "Dvoufaktorové ověřování přidává další vrstvu zabezpečení k vašemu účtu",
"EnableTwoFactorAuth": "Povolit dvoufaktorové ověřování",
+1
View File
@@ -229,6 +229,7 @@
"SpaceMembersOnly": "Nur Bereichsmitglieder",
"Reset": "Zurücksetzen",
"Security": "Sicherheit",
"SecurityTabSessions": "Sitzungsverlauf",
"TwoFactorAuth": "Zweistufige Authentifizierung",
"TwoFactorAuthDescription": "Zweistufige Authentifizierung fügt eine zusätzliche Sicherheitsebene zu Ihrem Konto hinzu",
"EnableTwoFactorAuth": "Zweistufige Authentifizierung aktivieren",
+1
View File
@@ -229,6 +229,7 @@
"ShowInTitle": "Show in title",
"SpaceMembersOnly": "Space members only",
"Security": "Security",
"SecurityTabSessions": "Session history",
"TwoFactorAuth": "Two-factor authentication",
"TwoFactorAuthDescription": "Two-factor authentication adds an extra layer of security to your account",
"EnableTwoFactorAuth": "Enable two-factor authentication",
+1
View File
@@ -220,6 +220,7 @@
"SpaceMembersOnly": "Solo miembros del espacio",
"Reset": "Reiniciar",
"Security": "Seguridad",
"SecurityTabSessions": "Historial de sesiones",
"TwoFactorAuth": "Autenticación de dos factores",
"TwoFactorAuthDescription": "La autenticación de dos factores añade una capa adicional de seguridad a tu cuenta",
"EnableTwoFactorAuth": "Habilitar autenticación de dos factores",
+1
View File
@@ -229,6 +229,7 @@
"SpaceMembersOnly": "Membres de l'espace uniquement",
"Reset": "Réinitialiser",
"Security": "Sécurité",
"SecurityTabSessions": "Historique des sessions",
"TwoFactorAuth": "Authentification à deux facteurs",
"TwoFactorAuthDescription": "L'authentification à deux facteurs ajoute une couche de sécurité supplémentaire à votre compte",
"EnableTwoFactorAuth": "Activer l'authentification à deux facteurs",
+1
View File
@@ -229,6 +229,7 @@
"SpaceMembersOnly": "Solo membri dello spazio",
"Reset": "Reset",
"Security": "Sicurezza",
"SecurityTabSessions": "Cronologia sessioni",
"TwoFactorAuth": "Autenticazione a due fattori",
"TwoFactorAuthDescription": "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al tuo account",
"EnableTwoFactorAuth": "Abilita autenticazione a due fattori",
+1
View File
@@ -229,6 +229,7 @@
"SpaceMembersOnly": "スペースメンバーのみ",
"Reset": "リセット",
"Security": "セキュリティ",
"SecurityTabSessions": "セッション履歴",
"TwoFactorAuth": "二要素認証",
"TwoFactorAuthDescription": "二要素認証はアカウントにセキュリティの追加レイヤーを追加します",
"EnableTwoFactorAuth": "二要素認証を有効にする",
+1
View File
@@ -220,6 +220,7 @@
"SpaceMembersOnly": "Apenas membros do espaço",
"Reset": "Reiniciar",
"Security": "Segurança",
"SecurityTabSessions": "Histórico de sessões",
"TwoFactorAuth": "Autenticação de dois fatores",
"TwoFactorAuthDescription": "A autenticação de dois fatores adiciona uma camada extra de segurança à sua conta",
"EnableTwoFactorAuth": "Ativar autenticação de dois fatores",
+1
View File
@@ -220,6 +220,7 @@
"SpaceMembersOnly": "Apenas membros do espaço",
"Reset": "Reiniciar",
"Security": "Segurança",
"SecurityTabSessions": "Histórico de sessões",
"TwoFactorAuth": "Autenticação de dois fatores",
"TwoFactorAuthDescription": "A autenticação de dois fatores adiciona uma camada extra de segurança à sua conta",
"EnableTwoFactorAuth": "Ativar autenticação de dois fatores",
+1
View File
@@ -229,6 +229,7 @@
"ShowInTitle": "Показывать в заголовке",
"SpaceMembersOnly": "Только участники пространства",
"Security": "Безопасность",
"SecurityTabSessions": "История сессий",
"TwoFactorAuth": "Двухфакторная аутентификация",
"TwoFactorAuthDescription": "Двухфакторная аутентификация добавляет дополнительный уровень безопасности к вашей учетной записи",
"EnableTwoFactorAuth": "Включить двухфакторную аутентификацию",
+1
View File
@@ -229,6 +229,7 @@
"SpaceMembersOnly": "Yalnızca alan üyeleri",
"Reset": "Sıfırla",
"Security": "Güvenlik",
"SecurityTabSessions": "Oturum geçmişi",
"TwoFactorAuth": "İki faktörlü kimlik doğrulama",
"TwoFactorAuthDescription": "İki faktörlü kimlik doğrulama hesabınıza ek bir güvenlik katmanı ekler",
"EnableTwoFactorAuth": "İki faktörlü kimlik doğrulamayı etkinleştir",
+1
View File
@@ -229,6 +229,7 @@
"SpaceMembersOnly": "仅限空间成员",
"Reset": "重置",
"Security": "安全",
"SecurityTabSessions": "会话历史",
"TwoFactorAuth": "双因素认证",
"TwoFactorAuthDescription": "双因素认证为您的帐户增加额外的安全层",
"EnableTwoFactorAuth": "启用双因素认证",
@@ -0,0 +1,75 @@
<!--
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import setting from '@hcengineering/setting'
import view from '@hcengineering/view'
import {
Breadcrumb,
defineSeparators,
Header,
NavItem,
Scroller,
Separator,
twoPanelsSeparators
} from '@hcengineering/ui'
import settingsRes from '../plugin'
import SessionHistorySettings from './SessionHistorySettings.svelte'
import TwoFactorSettings from './TwoFactorSettings.svelte'
let securityTab: 'twoFactor' | 'sessions' = 'twoFactor'
defineSeparators('securitySettings', twoPanelsSeparators)
</script>
<div class="hulyComponent">
<Header adaptive={'disabled'}>
<Breadcrumb icon={setting.icon.Password} label={setting.string.Security} size={'large'} isCurrent />
</Header>
<div class="hulyComponent-content__container columns">
<div class="hulyComponent-content__column navigation py-2">
<Scroller shrink>
<NavItem
icon={setting.icon.Password}
label={setting.string.TwoFactorAuth}
selected={securityTab === 'twoFactor'}
on:click={() => {
securityTab = 'twoFactor'
}}
/>
<NavItem
icon={view.icon.Timeline}
label={settingsRes.string.SecurityTabSessions}
selected={securityTab === 'sessions'}
on:click={() => {
securityTab = 'sessions'
}}
/>
</Scroller>
</div>
<Separator name={'securitySettings'} index={0} color={'var(--theme-divider-color)'} />
<div class="hulyComponent-content__column content">
<Scroller align={'center'} padding={'var(--spacing-3)'} bottomPadding={'var(--spacing-3)'}>
{#if securityTab === 'twoFactor'}
<TwoFactorSettings />
{:else}
<SessionHistorySettings />
{/if}
</Scroller>
</div>
</div>
</div>
@@ -0,0 +1,170 @@
<!--
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import type { SecurityLoginHistoryEvent } from '@hcengineering/account-client'
import { MessageBox } from '@hcengineering/presentation'
import { Button, Label, showPopup } from '@hcengineering/ui'
import { onMount } from 'svelte'
import settingsRes from '../plugin'
import { formatLocation, getShortUserAgent, maskIpAddress, shouldShowNotMeAction } from '../securityLoginActivity'
import { getAccountClient } from '../utils'
/** Narrow account client for security APIs (params match server contract). */
const accountClient = getAccountClient() as unknown as {
getMySecurityLoginHistory: (params?: { limit?: number, redact?: boolean }) => Promise<SecurityLoginHistoryEvent[]>
reportSecurityLoginConcern: (params?: { loginEventId?: string }) => Promise<void>
}
const recentActivityLimit = 20
let loginHistory: SecurityLoginHistoryEvent[] = []
let loginHistoryLoading = false
let loginHistoryLoaded = false
let loginHistoryError = false
async function loadRecentLoginActivity (): Promise<void> {
loginHistoryLoading = true
loginHistoryError = false
try {
loginHistory = await accountClient.getMySecurityLoginHistory({ limit: recentActivityLimit, redact: true })
loginHistoryLoaded = true
} catch (err: any) {
loginHistoryError = true
Analytics.handleError(err)
} finally {
loginHistoryLoading = false
}
}
function handleNotMeAction (event?: SecurityLoginHistoryEvent): void {
showPopup(MessageBox, {
label: settingsRes.string.NotMeDialogTitle,
message: settingsRes.string.NotMeDialogMessage,
okLabel: settingsRes.string.NotMeDialogAction,
action: async () => {
await accountClient.reportSecurityLoginConcern(event !== undefined ? { loginEventId: event.id } : {})
Analytics.handleEvent('Settings:RecentLoginActivityNotMe', {
eventId: event?.id ?? 'header-action',
authMethod: event?.authMethod ?? 'unknown'
})
}
})
}
onMount(() => {
void loadRecentLoginActivity()
})
</script>
<div class="flex-col gap-2 max-w-240">
<div class="flex-between">
<h3 class="text-lg font-medium"><Label label={settingsRes.string.RecentLoginActivityTitle} /></h3>
<Button
label={settingsRes.string.NotMeAction}
kind="secondary"
on:click={() => {
handleNotMeAction()
}}
/>
</div>
{#if loginHistoryLoading}
<div class="login-placeholder"><Label label={settingsRes.string.RecentLoginActivityLoading} /></div>
<div class="login-placeholder"><Label label={settingsRes.string.RecentLoginActivityLoading} /></div>
<div class="login-placeholder"><Label label={settingsRes.string.RecentLoginActivityLoading} /></div>
{:else if loginHistoryError}
<div class="login-empty">
<div><Label label={settingsRes.string.RecentLoginActivityError} /></div>
<Button
label={settingsRes.string.RecentLoginActivityRetry}
kind="secondary"
on:click={() => {
void loadRecentLoginActivity()
}}
/>
</div>
{:else if loginHistoryLoaded && loginHistory.length === 0}
<div class="login-empty"><Label label={settingsRes.string.RecentLoginActivityEmpty} /></div>
{:else}
<div class="flex-col gap-2">
{#each loginHistory as loginEvent (loginEvent.id)}
<div class="login-activity-row">
<div class="flex-between gap-2">
<span class="text-sm">{new Date(loginEvent.eventTime).toLocaleString()}</span>
<span class:login-success={loginEvent.success} class:login-failed={!loginEvent.success}>
{#if loginEvent.success}
<Label label={settingsRes.string.RecentLoginActivitySuccess} />
{:else}
<Label label={settingsRes.string.RecentLoginActivityFailure} />
{/if}
</span>
</div>
<div class="text-sm flex-row-center gap-1">
<Label label={settingsRes.string.RecentLoginActivityMethod} />: {loginEvent.authMethod}
</div>
<div class="text-sm flex-row-center gap-1">
<Label label={settingsRes.string.RecentLoginActivityIp} />: {maskIpAddress(loginEvent.ip)}
</div>
<div class="text-sm flex-row-center gap-1">
<Label label={settingsRes.string.RecentLoginActivityLocation} />:
{formatLocation({ city: loginEvent.city, country: loginEvent.country })}
</div>
<div class="text-sm flex-row-center gap-1">
<Label label={settingsRes.string.RecentLoginActivityDevice} />:
{getShortUserAgent(loginEvent.userAgent)}
</div>
{#if shouldShowNotMeAction(loginEvent)}
<div class="mt-2">
<Button
label={settingsRes.string.NotMeAction}
kind="secondary"
on:click={() => {
handleNotMeAction(loginEvent)
}}
/>
</div>
{/if}
</div>
{/each}
</div>
{/if}
</div>
<style lang="scss">
.login-activity-row {
border: 1px solid var(--theme-divider-color, var(--divider-color));
border-radius: 0.5rem;
padding: 0.75rem;
}
.login-placeholder {
height: 2.25rem;
border-radius: 0.5rem;
background: var(--theme-button-default, var(--button-default));
opacity: 0.35;
}
.login-empty {
color: var(--caption-color);
font-size: 0.875rem;
}
.login-success {
color: var(--theme-positive-color, #2e7d32);
}
.login-failed {
color: var(--theme-danger-color, #d32f2f);
}
</style>
@@ -17,7 +17,7 @@
import { getCurrentAccount } from '@hcengineering/core'
import { OK, unknownError, getEmbeddedLabel } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { Breadcrumb, Button, EditBox, Header, Label, Spinner, Status } from '@hcengineering/ui'
import { Button, EditBox, Label, Spinner, Status } from '@hcengineering/ui'
import { getAccountClient } from '../utils'
import presentation from '@hcengineering/presentation'
@@ -96,10 +96,6 @@
</script>
<div class="hulyComponent">
<Header adaptive={'disabled'}>
<Breadcrumb icon={setting.icon.Password} label={setting.string.TwoFactorAuth} size="large" isCurrent />
</Header>
<div class="flex-col p-6 gap-8 max-w-2xl">
<div class="flex flex-between">
<Label label={setting.string.TwoFactorAuthDescription} />
+3 -1
View File
@@ -30,6 +30,7 @@ import Password from './components/Password.svelte'
import Privacy from './components/Privacy.svelte'
import Profile from './components/Profile.svelte'
import Settings from './components/Settings.svelte'
import SecuritySettings from './components/SecuritySettings.svelte'
import TwoFactorSettings from './components/TwoFactorSettings.svelte'
import { Analytics } from '@hcengineering/analytics'
@@ -171,7 +172,8 @@ export default async (): Promise<Resources> => ({
AddEmailSocialId,
EmployeeRefEditor,
UserRoleSelect,
TwoFactorSettings
TwoFactorSettings,
SecuritySettings
},
actionImpl: {
DeleteMixin
+2 -1
View File
@@ -178,6 +178,7 @@ export default mergeIds(settingId, setting, {
NotMeAction: '' as IntlString,
NotMeDialogTitle: '' as IntlString,
NotMeDialogMessage: '' as IntlString,
NotMeDialogAction: '' as IntlString
NotMeDialogAction: '' as IntlString,
SecurityTabSessions: '' as IntlString
}
})
+2 -1
View File
@@ -246,7 +246,8 @@ export default plugin(settingId, {
AddEmailSocialId: '' as AnyComponent,
OfficeSettings: '' as AnyComponent,
UserRoleSelect: '' as AnyComponent,
TwoFactorSettings: '' as AnyComponent
TwoFactorSettings: '' as AnyComponent,
SecuritySettings: '' as AnyComponent
},
string: {
Settings: '' as IntlString,