2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 11:27:17 +08:00

feat: 用户支持开启2FA

This commit is contained in:
2025-05-14 19:04:03 +08:00
parent 462d6c0789
commit 5fd00acd48
15 changed files with 476 additions and 88 deletions

View File

@@ -15,5 +15,22 @@ export default {
// 是否登录
isLogin: () => http.Get('/user/is_login'),
// 获取用户信息
info: () => http.Get('/user/info')
info: () => http.Get('/user/info'),
// 获取用户列表
list: (page: number, limit: number): any => http.Get(`/users`, { params: { page, limit } }),
// 创建用户
create: (username: string, password: string, email: string): any =>
http.Post('/users', { username, password, email }),
// 删除用户
delete: (id: number): any => http.Delete(`/users/${id}`),
// 更新用户邮箱
updateEmail: (id: number, email: string): any => http.Post(`/users/${id}/email`, { email }),
// 更新用户密码
updatePassword: (id: number, password: string): any =>
http.Post(`/users/${id}/password`, { password }),
// 生成2FA密钥
generateTwoFA: (id: number): any => http.Get(`/users/${id}/2fa`),
// 保存2FA密钥
updateTwoFA: (id: number, code: string, secret: string): any =>
http.Post(`/users/${id}/2fa`, { code, secret })
}

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import setting from '@/api/panel/setting'
defineOptions({
name: 'setting-index'
})
import { NButton } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import setting from '@/api/panel/setting'
import TheIcon from '@/components/custom/TheIcon.vue'
import { useThemeStore } from '@/store'
import SettingBase from '@/views/setting/SettingBase.vue'
import SettingSafe from '@/views/setting/SettingSafe.vue'
import { NButton } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import SettingUser from '@/views/setting/SettingUser.vue'
const { $gettext } = useGettext()
const themeStore = useThemeStore()
@@ -21,10 +21,6 @@ const { data: model } = useRequest(setting.list, {
name: '',
channel: 'stable',
locale: 'en',
channel: 'stable',
username: '',
password: '',
email: '',
port: 8888,
entrance: '',
offline_mode: false,
@@ -53,15 +49,21 @@ const handleSave = () => {
}
})
}
const handleCreate = () => {}
</script>
<template>
<common-page show-footer>
<template #action>
<n-button type="primary" @click="handleSave">
<n-button v-if="currentTab != 'user'" type="primary" @click="handleSave">
<TheIcon :size="18" icon="material-symbols:save-outline" />
{{ $gettext('Save') }}
</n-button>
<n-button v-if="currentTab == 'user'" type="primary" @click="handleCreate">
<TheIcon :size="18" icon="material-symbols:add" />
{{ $gettext('Create User') }}
</n-button>
</template>
<n-tabs v-model:value="currentTab" type="line" animated>
<n-tab-pane name="base" :tab="$gettext('Basic')">
@@ -70,6 +72,9 @@ const handleSave = () => {
<n-tab-pane name="safe" :tab="$gettext('Safe')">
<setting-safe v-model:model="model" />
</n-tab-pane>
<n-tab-pane name="user" :tab="$gettext('User')">
<setting-user />
</n-tab-pane>
</n-tabs>
</common-page>
</template>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import user from '@/api/panel/user'
import { NButton, NInput } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
const show = defineModel<boolean>('show', { type: Boolean, required: true })
const id = defineModel<number>('id', { type: Number, required: true })
const model = ref({
password: ''
})
const handleUpdate = () => {
useRequest(() => user.updatePassword(id.value, model.value.password)).onSuccess(() => {
show.value = false
window.$message.success($gettext('Updated successfully'))
window.$bus.emit('user:refresh')
})
}
</script>
<template>
<n-modal
v-model:show="show"
preset="card"
:title="$gettext('Change Password')"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
@close="show = false"
>
<n-form :model="model">
<n-form-item path="password" :label="$gettext('Password')">
<n-input
v-model:value="model.password"
type="password"
show-password-on="click"
@keydown.enter.prevent
:placeholder="$gettext('Enter user password')"
/>
</n-form-item>
</n-form>
<n-button type="info" block @click="handleUpdate">{{ $gettext('Submit') }}</n-button>
</n-modal>
</template>
<style scoped lang="scss"></style>

View File

@@ -46,15 +46,6 @@ const channels = [
<n-form-item :label="$gettext('Update Channel')">
<n-select v-model:value="model.channel" :options="channels"> </n-select>
</n-form-item>
<n-form-item :label="$gettext('Username')">
<n-input v-model:value="model.username" :placeholder="$gettext('admin')" />
</n-form-item>
<n-form-item :label="$gettext('Password')">
<n-input v-model:value="model.password" :placeholder="$gettext('admin')" />
</n-form-item>
<n-form-item :label="$gettext('Certificate Default Email')">
<n-input v-model:value="model.email" :placeholder="$gettext('admin@yourdomain.com')" />
</n-form-item>
<n-form-item :label="$gettext('Port')">
<n-input-number v-model:value="model.port" :placeholder="$gettext('8888')" w-full />
</n-form-item>

View File

@@ -0,0 +1,181 @@
<script setup lang="ts">
import user from '@/api/panel/user'
import { formatDateTime, renderIcon } from '@/utils'
import PasswordModal from '@/views/setting/PasswordModal.vue'
import TwoFaModal from '@/views/setting/TwoFaModal.vue'
import { NButton, NDataTable, NInput, NPopconfirm, NSwitch } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
const currentID = ref(0)
const passwordModal = ref(false)
const twoFaModal = ref(false)
const columns: any = [
{
title: $gettext('Username'),
key: 'username',
minWidth: 100,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: $gettext('Email'),
key: 'email',
minWidth: 200,
resizable: true,
ellipsis: { tooltip: true },
render(row: any) {
return h(NInput, {
size: 'small',
value: row.email,
onBlur: () => handleEmail(row),
onUpdateValue(v) {
row.email = v
}
})
}
},
{
title: $gettext('2FA'),
key: 'two_fa',
width: 150,
render(row: any) {
return h(NSwitch, {
size: 'small',
rubberBand: false,
value: row.two_fa !== '',
onUpdateValue: (v) => {
console.log(v)
if (v) {
twoFaModal.value = true
currentID.value = row.id
} else {
useRequest(user.updateTwoFA(row.id, '', '')).onSuccess(() => {
window.$message.success($gettext('Disabled successfully'))
refresh()
})
}
}
})
}
},
{
title: $gettext('Creation Time'),
key: 'created_at',
minWidth: 200,
ellipsis: { tooltip: true },
render(row: any) {
return formatDateTime(row.created_at)
}
},
{
title: $gettext('Actions'),
key: 'actions',
width: 260,
hideInExcel: true,
render(row: any) {
return [
h(
NButton,
{
size: 'small',
type: 'primary',
onClick: () => {
currentID.value = row.id
passwordModal.value = true
}
},
{
default: () => $gettext('Change Password'),
icon: renderIcon('material-symbols:edit-outline', { size: 14 })
}
),
h(
NPopconfirm,
{
style: 'margin-left: 15px;',
onPositiveClick: () => handleDelete(row.id)
},
{
default: () => {
return $gettext('Are you sure you want to delete this user?')
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'error',
style: 'margin-left: 15px;'
},
{
default: () => $gettext('Delete'),
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
}
)
}
}
)
]
}
}
]
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
(page, pageSize) => user.list(page, pageSize),
{
initialData: { total: 0, list: [] },
initialPageSize: 20,
total: (res: any) => res.total,
data: (res: any) => res.items
}
)
const handleEmail = (row: any) => {
useRequest(user.updateEmail(row.id, row.email)).onSuccess(() => {
window.$message.success($gettext('Modified successfully'))
})
}
const handleDelete = (id: number) => {
useRequest(user.delete(id)).onSuccess(() => {
window.$message.success($gettext('Deleted successfully'))
refresh()
})
}
onMounted(() => {
window.$bus.on('user:refresh', refresh)
})
</script>
<template>
<n-flex vertical>
<n-data-table
striped
remote
:scroll-x="1000"
:loading="loading"
:columns="columns"
:data="data"
:row-key="(row: any) => row.name"
v-model:page="page"
v-model:pageSize="pageSize"
:pagination="{
page: page,
pageCount: pageCount,
pageSize: pageSize,
itemCount: total,
showQuickJumper: true,
showSizePicker: true,
pageSizes: [20, 50, 100, 200]
}"
/>
</n-flex>
<password-modal v-model:id="currentID" v-model:show="passwordModal" />
<two-fa-modal v-model:id="currentID" v-model:show="twoFaModal" />
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import user from '@/api/panel/user'
import { NButton, NInput } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
const show = defineModel<boolean>('show', { type: Boolean, required: true })
const id = defineModel<number>('id', { type: Number, required: true })
const model = ref({
img: '',
url: '',
secret: ''
})
const code = ref('')
const qrCode = computed(() => {
return `data:image/png;base64,${model.value.img}`
})
const handleUpdate = () => {
useRequest(() => user.updateTwoFA(id.value, code.value, model.value.secret)).onSuccess(() => {
show.value = false
code.value = ''
window.$message.success($gettext('Updated successfully'))
window.$bus.emit('user:refresh')
})
}
watch(
() => show.value,
(val) => {
if (val) {
useRequest(() => user.generateTwoFA(id.value)).onSuccess(({ data }) => {
model.value = data
})
}
},
{ immediate: true }
)
</script>
<template>
<n-modal
v-model:show="show"
preset="card"
:title="$gettext('Enable 2FA')"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
@close="show = false"
>
<n-flex vertical>
<n-flex :wrap="false" justify="start" align="flex-start" :size="20">
<n-image :src="qrCode" :alt="$gettext('QR Code')" width="200" height="200" />
<n-flex vertical :size="12">
<n-text style="max-width: 400px">
{{ $gettext('Scan the QR code with your 2FA app and enter the code below') }}
</n-text>
<n-text style="max-width: 400px">
{{
$gettext('If you cannot scan the QR code, please enter the URL below in your 2FA app')
}}
</n-text>
<n-text style="max-width: 400px; word-break: break-all">
{{ model.url }}
</n-text>
</n-flex>
</n-flex>
<n-form>
<n-form-item path="code" :label="$gettext('Code')">
<n-input
v-model:value="code"
@keydown.enter.prevent
:placeholder="$gettext('Enter the code')"
/>
</n-form-item>
</n-form>
<n-button type="info" block @click="handleUpdate">{{ $gettext('Submit') }}</n-button>
</n-flex>
</n-modal>
</template>
<style scoped lang="scss"></style>