mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-12 20:57:45 +02:00
Move security section
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user