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
107 changes: 107 additions & 0 deletions src/layouts/modals/AdminEnable2FA.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="auto">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('admin.enable2FA') + " " + user.username }}
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col>{{ $t('admin.rescanQr') }}</v-col>
</v-row>
<v-row>
<v-col>{{ $t('admin.rescanQrHint') }}</v-col>
</v-row>
<v-row>
<v-col class="d-flex justify-center">
<QrcodeVue :value="newData.secretUrl" :size="size" :margin="1" style="border-radius: 1rem;" />
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field v-model="newData.passcode" :label="$t('admin.passcode')" :rules="passcodeRules" required></v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
variant="outlined"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
variant="tonal"
@click="enable2FA"
>
{{ $t('actions.enable') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script lang="ts">
import QrcodeVue from 'qrcode.vue'
import HttpUtils from '@/plugins/httputil'
import { i18n } from '@/locales'

export default {
props: ['visible', 'user'],
data() {
return {
newData: {
secretUrl: "",
passcode: "",
loading: false
},
passcodeRules: [
(value: string) => {
if (value?.length == 6) return true
return i18n.global.t('login.passcodeRules')
},
]
}
},
methods: {
async load() {
this.newData.loading = true
const response = await HttpUtils.get('api/prepare2fa')
this.newData.loading = false
if (response.success) {
this.newData.secretUrl = response.obj
}
},
resetData() {
this.newData.secretUrl = ""
this.newData.passcode = ""
},
closeModal() {
this.resetData() // reset
this.$emit('close')
},
enable2FA() {
this.$emit('save', this.newData)
},
},
computed: {
size() {
if (window.innerWidth > 380) return 300
if (window.innerWidth > 330) return 280
return 250
},
},
watch: {
visible(newValue) {
if (newValue) {
this.resetData()
this.load()
}
},
},
components: { QrcodeVue }
}

</script>
12 changes: 12 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
all: "All",
filter: "Filter",
loading: "Loading...",
warning: "Warning",
confirm: "Are you sure ?",
yes: "yes",
no: "no",
Expand Down Expand Up @@ -119,6 +120,8 @@ export default {
addbulk: "Add Bulk",
new: "New",
edit: "Edit",
edit2fa: "Edit 2FA",
enable: "Enable",
del: "Delete",
clone: "Clone",
save: "Save",
Expand All @@ -137,12 +140,21 @@ export default {
unRules: "Username can not be empty",
password: "Password",
pwRules: "Password can not be empty",
passcode: "Passcode",
passcodeRules: "Passcode can not be empty",
},
menu: {
logout: "Logout",
},
admin: {
changeCred: "Change credentials",
enable2FA: "Enable two-factor authentication (2FA)",
rescanQr: "Re-scan the QR code",
rescanQrHint: "Use an authenticator app from your phone to scan.",
passcode: "Verify the code from the app",
confirm2FAdisable: "Are you sure do disable 2FA?",
enabled: "Enabled",
disabled: "Disabled",
oldPass: "Current Password",
newUname: "New Username",
newPass: "New Password",
Expand Down
12 changes: 12 additions & 0 deletions src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
all: "Все",
filter: "Фильтр",
loading: "Загрузка...",
warning: "Внимание",
confirm: "Вы уверены?",
yes: "да",
no: "нет",
Expand Down Expand Up @@ -119,6 +120,8 @@ export default {
addbulk: "Добавить пакетно",
new: "Новый",
edit: "Редактировать",
edit2fa: "Редактировать 2FA",
enable: "Включить",
del: "Удалить",
clone: "Клонировать",
save: "Сохранить",
Expand All @@ -137,12 +140,21 @@ export default {
unRules: "Имя пользователя не может быть пустым",
password: "Пароль",
pwRules: "Пароль не может быть пустым",
passcode: "Одноразовый код",
passcodeRules: "Одноразовый код не может быть пустым",
},
menu: {
logout: "Выйти",
},
admin: {
changeCred: "Изменить учетные данные",
enable2FA: "Включить двухфакторную аутенификацию (2FA)",
rescanQr: "Отсканируйте QR-код",
rescanQrHint: "Используйте приложение для аутенификации на телефоне чтобы отсканировать.",
passcode: "Введите код из приложения",
confirm2FAdisable: "Вы уверены что хотите выключить 2FA?",
enabled: "Включена",
disabled: "Отключена",
oldPass: "Текущий пароль",
newUname: "Новое имя пользователя",
newPass: "Новый пароль",
Expand Down
86 changes: 86 additions & 0 deletions src/views/Admins.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
@close="closeEditModal"
@save="saveEditModal"
/>
<AdminEnable2FAModal
v-model="enable2FAModal.visible"
:visible="enable2FAModal.visible"
:user="enable2FAModal.user"
@close="closeEnable2FAModal"
@save="saveEnable2FAModal"
/>
<ChangeModal
v-model="changesModal.visible"
:visible="changesModal.visible"
Expand All @@ -18,6 +25,25 @@
:visible="tokenModal.visible"
@close="closeTokenModal"
/>
<v-dialog v-model="open2FAdisable" width="auto">
<v-card
max-width="400"
prepend-icon="mdi-alert"
:title="$t('warning')"
:text="$t('admin.confirm2FAdisable')">
<template v-slot:actions>
<v-btn
color="primary"
variant="outlined"
@click="open2FAdisable = false"
>{{ $t('close') }}</v-btn>
<v-btn
variant="tonal"
@click="saveDisable2FA()"
>{{ $t('disable') }}</v-btn>
</template>
</v-card>
</v-dialog>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showChangesModal('')" style="margin: 0 5px;">{{ $t('admin.changes') }}</v-btn>
Expand Down Expand Up @@ -48,13 +74,23 @@
<v-col>
{{ item.ip }}
</v-col>
</v-row>
<v-row>
<v-col>2FA</v-col>
<v-col>
{{ item.totp ? $t('admin.enabled') : $t('admin.disabled') }}
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-account-edit" @click="showEditModal(item)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
<v-btn icon="mdi-shield-lock-outline" @click="show2FAModal(item)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit2fa')"></v-tooltip>
</v-btn>
<v-btn icon="mdi-list-box-outline" @click="showChangesModal(item.username)">
<v-icon />
Expand All @@ -68,6 +104,7 @@

<script lang="ts" setup>
import AdminModal from '@/layouts/modals/Admin.vue'
import AdminEnable2FAModal from '@/layouts/modals/AdminEnable2FA.vue'
import ChangeModal from '@/layouts/modals/Changes.vue'
import TokenModal from '@/layouts/modals/Token.vue'
import { i18n } from '@/locales'
Expand All @@ -77,6 +114,7 @@ import { Ref, ref, inject, onMounted } from 'vue'
const loading:Ref = inject('loading')?? ref(false)

const users = ref(<any[]>[])
const open2FAdisable = ref(false)

onMounted(async () => {loadData()})

Expand All @@ -95,6 +133,7 @@ const loadData = async () => {
loginDate: loginDateTime[0],
loginTime: loginDateTime[1],
ip: lastLogin[2]?? "-",
totp: u.totp
})
})
}
Expand Down Expand Up @@ -132,6 +171,53 @@ const saveEditModal = async (data:any) => {
}
}

const show2FAModal = (user: any) => {
if (user.totp) {
open2FAdisable.value = true
} else {
showEnable2FAModal(user)
}
}

const enable2FAModal = ref({
visible: false,
user: {},
})

const showEnable2FAModal = (user: any) => {
enable2FAModal.value.user = user
enable2FAModal.value.visible = true
}
const closeEnable2FAModal = () => {
enable2FAModal.value.visible = false
enable2FAModal.value.user = {}
}
const saveEnable2FAModal = async (data: any) => {
loading.value=true
const response = await HttpUtils.post('api/enable2fa',data)
if(response.success){
setTimeout(() => {
loading.value=false
closeEnable2FAModal()
}, 500)
} else {
loading.value=false
}
}

const saveDisable2FA = async () => {
loading.value=true
const response = await HttpUtils.post('api/disable2fa', null)
if(response.success){
setTimeout(() => {
loading.value=false
open2FAdisable.value = false
}, 500)
} else {
loading.value=false
}
}

const changesModal = ref({
visible: false,
actor: '',
Expand Down
17 changes: 13 additions & 4 deletions src/views/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
<v-card-title class="headline" v-text="$t('login.title')"></v-card-title>
<v-card-text>
<v-form @submit.prevent="login" ref="form">
<v-text-field v-model="username" :label="$t('login.username')" :rules="usernameRules" required></v-text-field>
<v-text-field v-model="password" :label="$t('login.password')" :rules="passwordRules" type="password" required></v-text-field>
<v-text-field v-model="username" prepend-icon="mdi-account" :label="$t('login.username')" :rules="usernameRules" required></v-text-field>
<v-text-field v-model="password" prepend-icon="mdi-form-textbox-password" :label="$t('login.password')" :rules="passwordRules" type="password" required></v-text-field>
<v-text-field v-model="passcode" prepend-icon="mdi-shield-lock-outline" :label="$t('login.passcode')" :rules="passcodeRules" required></v-text-field>
<v-btn :loading="loading" type="submit" color="primary" block class="mt-2" v-text="$t('actions.submit')"></v-btn>
</v-form>
<v-select
Expand Down Expand Up @@ -79,13 +80,21 @@ const passwordRules = [
},
]

const passcode = ref('')
const passcodeRules = [
(value: string) => {
if (value?.length == 6) return true
return i18n.global.t('login.passcodeRules')
},
]

const loading = ref(false)
const router = useRouter()

const login = async () => {
if (username.value == '' || password.value == '') return
if (username.value == '' || password.value == '' || passcode.value == '') return
loading.value=true
const response = await HttpUtil.post('api/login',{user: username.value, pass: password.value})
const response = await HttpUtil.post('api/login',{user: username.value, pass: password.value, passcode: passcode.value})
if(response.success){
setTimeout(() => {
loading.value=false
Expand Down