Harden auth and API: username change, 2FA fixes, input validation
- Add POST /api/auth/change-username (password + OTP when 2FA is on, format/reserved-name checks, uniqueness) and a Change Username modal in Account.vue; use the real username for the 2FA provisioning URI. - Stop creating the built-in admin/password user; the initial admin is seeded only from OVPMON_INITIAL_ADMIN_USER/PASSWORD. - Reject 2FA-pending temporary tokens on all protected routes (Flask token_required, Profiler verify_token); only /api/auth/verify-2fa accepts them. - Stop logging the OTP and TOTP secret in enable_2fa. - Profiler: validate profile username (pattern + realpath checks in schema, router, pki and generator) to prevent path traversal. - Restrict CORS to the panel origin in Profiler and Flask APIs. - UI: header username no longer sticks to the hardcoded Admin fallback; it is synced from /user/me and updated after a rename. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
14ffd64801
commit
11c1b6379b
9 files changed
+241
-16
No files matched your search
+24
-2
@@ -99,7 +99,7 @@
|
||||
|
||||
<div class="user-profile ms-2">
|
||||
<div class="user-avatar-small bg-primary text-white">
|
||||
{{ username[0]?.toUpperCase() || 'A' }}
|
||||
{{ username[0]?.toUpperCase() || 'U' }}
|
||||
</div>
|
||||
<div class="user-meta d-none d-md-block">
|
||||
<span class="username">{{ username }}</span>
|
||||
@@ -123,9 +123,11 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, computed } from 'vue';
|
||||
import { useAppConfig } from './composables/useAppConfig';
|
||||
import { useApi } from './composables/useApi';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const { loadConfig, isLoaded } = useAppConfig();
|
||||
const { apiClient } = useApi();
|
||||
const timezoneAbbr = ref(new Date().toLocaleTimeString('en-us',{timeZoneName:'short'}).split(' ')[2] || 'UTC');
|
||||
const isDark = ref(false);
|
||||
const refreshKey = ref(0);
|
||||
@@ -135,7 +137,24 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const isAuthenticated = computed(() => route.name !== 'Login');
|
||||
const username = ref(localStorage.getItem('ovpmon_user') || 'Admin');
|
||||
const username = ref(localStorage.getItem('ovpmon_user') || '');
|
||||
|
||||
// Keep the header name in sync with the server (login, rename, stale session)
|
||||
const syncUsername = async () => {
|
||||
if (!localStorage.getItem('ovpmon_token')) {
|
||||
username.value = '';
|
||||
return;
|
||||
}
|
||||
username.value = localStorage.getItem('ovpmon_user') || '';
|
||||
try {
|
||||
const res = await apiClient.get('/user/me');
|
||||
if (res.data?.username) {
|
||||
username.value = res.data.username;
|
||||
localStorage.setItem('ovpmon_user', res.data.username);
|
||||
}
|
||||
} catch (e) { /* 401 is handled by the interceptor */ }
|
||||
};
|
||||
const onUserChanged = () => { username.value = localStorage.getItem('ovpmon_user') || ''; };
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('ovpmon_token');
|
||||
@@ -162,10 +181,13 @@ const refreshPage = () => {
|
||||
// Close sidebar on route change
|
||||
watch(() => route.path, () => {
|
||||
isSidebarOpen.value = false;
|
||||
if (route.name !== 'Login') syncUsername();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadConfig();
|
||||
window.addEventListener('ovpmon-user-changed', onUserChanged);
|
||||
if (route.name !== 'Login') syncUsername();
|
||||
|
||||
// Init Theme
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<button class="btn btn-action btn-action-primary py-2 fw-bold btn-account-action" @click="showPwModal">
|
||||
Change Password
|
||||
</button>
|
||||
<button class="btn btn-action btn-action-secondary py-2 fw-bold btn-account-action mt-2" @click="showUserModal">
|
||||
Change Username
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,6 +167,55 @@
|
||||
</button>
|
||||
</template>
|
||||
</BaseModal>
|
||||
|
||||
<!-- Username Change Modal -->
|
||||
<BaseModal id="usernameChangeModal" title="Change Account Username" ref="userModal">
|
||||
<template #body>
|
||||
<form @submit.prevent="handleChangeUsername" id="userForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase">New Username</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="usernameForm.new_username"
|
||||
placeholder="3-32 chars: letters, digits, . _ -"
|
||||
pattern="[A-Za-z][A-Za-z0-9_.\-]{2,31}"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase">Current Password</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
v-model="usernameForm.current_password"
|
||||
placeholder="Enter current password"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="mb-2" v-if="isEnabled">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase">2FA Code</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="usernameForm.otp"
|
||||
placeholder="6-digit code"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button type="button" class="btn-action btn-action-secondary" @click="userModal.hide()">Cancel</button>
|
||||
<button type="submit" form="userForm" class="btn-action btn-action-save" :disabled="userLoading">
|
||||
<span v-if="userLoading" class="spinner-border spinner-border-sm me-2"></span>
|
||||
Update Username
|
||||
</button>
|
||||
</template>
|
||||
</BaseModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -179,6 +231,7 @@ const { apiClient } = useApi();
|
||||
|
||||
// UI Refs
|
||||
const pwModal = ref(null);
|
||||
const userModal = ref(null);
|
||||
const confirmModal = ref(null);
|
||||
const disable2FAModal = ref(null);
|
||||
|
||||
@@ -234,6 +287,57 @@ const handleChangePassword = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Username Logic
|
||||
const userLoading = ref(false);
|
||||
const usernameForm = reactive({ new_username: '', current_password: '', otp: '' });
|
||||
|
||||
const showUserModal = () => {
|
||||
usernameForm.new_username = '';
|
||||
usernameForm.current_password = '';
|
||||
usernameForm.otp = '';
|
||||
userModal.value.show();
|
||||
};
|
||||
|
||||
const handleChangeUsername = async () => {
|
||||
if (!/^[A-Za-z][A-Za-z0-9_.-]{2,31}$/.test(usernameForm.new_username)) {
|
||||
Swal.fire({
|
||||
title: 'Error!',
|
||||
text: 'Username must be 3-32 chars, start with a letter, and contain only letters, digits, . _ -',
|
||||
icon: 'error',
|
||||
confirmButtonColor: '#EC7C31'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
userLoading.value = true;
|
||||
try {
|
||||
const res = await apiClient.post('../auth/change-username', {
|
||||
new_username: usernameForm.new_username,
|
||||
current_password: usernameForm.current_password,
|
||||
otp: usernameForm.otp || undefined
|
||||
});
|
||||
|
||||
try { localStorage.setItem('ovpmon_user', res.data.username); } catch (e) { /* ignore */ }
|
||||
window.dispatchEvent(new Event('ovpmon-user-changed'));
|
||||
userModal.value.hide();
|
||||
Swal.fire({
|
||||
title: 'Success!',
|
||||
text: 'Username updated. Use the new username the next time you sign in.',
|
||||
icon: 'success',
|
||||
confirmButtonColor: '#1652B8'
|
||||
});
|
||||
} catch (err) {
|
||||
Swal.fire({
|
||||
title: 'Failed',
|
||||
text: err.response?.data?.error || 'Failed to update username',
|
||||
icon: 'error',
|
||||
confirmButtonColor: '#cf222e'
|
||||
});
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 2FA Logic
|
||||
const isEnabled = ref(false);
|
||||
const step2 = ref(false);
|
||||
|
||||
Reference in new issue
Block a user