Initial commit — Stupid Simple Network Inventory

Application web d'inventaire réseau manuel avec FastAPI, Vue 3 et Docker.
Inclut l'authentification JWT, la découverte ICMP, et la topologie en cards CSS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 09:19:19 +02:00
commit 88cf6458d0
58 changed files with 10365 additions and 0 deletions
+279
View File
@@ -0,0 +1,279 @@
<template>
<div class="modal-overlay" @click.self="forced ? null : $emit('close')">
<div class="modal">
<div class="modal-header">
<h2>{{ t('accountSettings') }}</h2>
<button v-if="!forced" class="close-btn" @click="$emit('close')"></button>
</div>
<div class="modal-body">
<div v-if="forced" class="forced-warning">
{{ t('mustChangePasswordWarning') }}
</div>
<p class="current-user">{{ t('loginUsername') }} : <strong>{{ currentUsername }}</strong></p>
<div class="section-title">{{ t('newUsername') }}</div>
<div class="field">
<input
v-model="newUsername"
type="text"
:placeholder="currentUsername"
autocomplete="username"
/>
</div>
<div class="section-title">
{{ t('newPassword') }}
<span v-if="forced" class="required-star">*</span>
</div>
<div class="field">
<input
v-model="newPassword"
type="password"
:placeholder="forced ? '' : t('leaveBlankToKeep')"
autocomplete="new-password"
/>
</div>
<template v-if="newPassword || forced">
<div class="section-title">{{ t('confirmPassword') }}</div>
<div class="field">
<input
v-model="confirmPassword"
type="password"
autocomplete="new-password"
/>
</div>
</template>
<div class="section-title">{{ t('currentPassword') }} *</div>
<div class="field">
<input
v-model="currentPassword"
type="password"
autocomplete="current-password"
required
/>
</div>
<p v-if="errorMsg" class="error-msg">{{ errorMsg }}</p>
<p v-if="successMsg" class="success-msg">{{ successMsg }}</p>
</div>
<div class="modal-footer">
<button v-if="!forced" class="btn-cancel" @click="$emit('close')">{{ t('cancel') }}</button>
<button class="btn-save" :disabled="saving" @click="save">
{{ saving ? '' : t('save') }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { authApi } from '../api.js'
import { t } from '../i18n.js'
import { currentUsername } from '../auth.js'
const props = defineProps({ forced: { type: Boolean, default: false } })
const emit = defineEmits(['close', 'updated'])
const newUsername = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const currentPassword = ref('')
const errorMsg = ref('')
const successMsg = ref('')
const saving = ref(false)
const _USERNAME_RE = /^[a-zA-Z0-9._-]{1,64}$/
function _validateClient() {
if (props.forced && !newPassword.value) {
errorMsg.value = t('newPasswordRequired')
return false
}
if (newUsername.value && !_USERNAME_RE.test(newUsername.value)) {
errorMsg.value = t('usernameInvalid')
return false
}
if (newPassword.value) {
if (newPassword.value.length < 8) {
errorMsg.value = t('passwordTooShort')
return false
}
if (!/[a-zA-Z]/.test(newPassword.value) || !/[0-9]/.test(newPassword.value)) {
errorMsg.value = t('passwordTooWeak')
return false
}
if (newPassword.value !== confirmPassword.value) {
errorMsg.value = t('passwordMismatch')
return false
}
}
return true
}
async function save() {
errorMsg.value = ''
successMsg.value = ''
if (!currentPassword.value) {
errorMsg.value = t('currentPassword') + ' ' + t('loginPassword').toLowerCase()
return
}
if (!_validateClient()) return
if (!newUsername.value && !newPassword.value) return
saving.value = true
try {
const payload = { current_password: currentPassword.value }
if (newUsername.value) payload.new_username = newUsername.value
if (newPassword.value) payload.new_password = newPassword.value
const { data } = await authApi.updateAccount(payload)
successMsg.value = t('accountUpdated')
emit('updated', {
token: data.access_token,
username: data.username,
mustChangePassword: data.must_change_password || false,
})
newUsername.value = ''
newPassword.value = ''
confirmPassword.value = ''
currentPassword.value = ''
} catch (e) {
const detail = e.response?.data?.detail || ''
const MAP = {
'Current password is incorrect': 'wrongPassword',
'password_too_short': 'passwordTooShort',
'password_too_weak': 'passwordTooWeak',
'username_invalid': 'usernameInvalid',
}
errorMsg.value = t(MAP[detail] || 'saveError')
} finally {
saving.value = false
}
}
</script>
<style scoped>
.modal-overlay {
position: fixed; inset: 0;
background: var(--modal-overlay);
display: flex; align-items: center; justify-content: center;
z-index: 1000;
}
.modal {
background: var(--bg-card);
border: 1.5px solid var(--border);
border-radius: 14px;
width: 100%;
max-width: 420px;
box-shadow: var(--shadow-modal);
display: flex;
flex-direction: column;
}
.modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 20px 14px;
border-bottom: 1px solid var(--border);
}
.modal-header h2 { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.close-btn {
background: none; border: none; font-size: 16px;
color: var(--text-muted); cursor: pointer; line-height: 1;
}
.close-btn:hover { color: var(--text-primary); }
.modal-body {
padding: 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.forced-warning {
font-size: 13px;
color: #92400E;
background: #FEF3C7;
border: 1px solid #FCD34D;
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 4px;
}
:global(html.dark .forced-warning) {
background: #2D2000;
border-color: #5C4200;
color: #FCD34D;
}
.required-star { color: #EF4444; margin-left: 2px; }
.current-user {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.current-user strong { color: var(--text-primary); }
.section-title {
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
margin-top: 4px;
}
.field input {
width: 100%;
padding: 8px 11px;
border: 1.5px solid var(--border);
border-radius: 7px;
font-size: 13px;
background: var(--bg-input);
color: var(--text-primary);
outline: none;
transition: border-color 0.15s;
}
.field input:focus { border-color: #3B82F6; }
.error-msg {
font-size: 12px; color: #EF4444;
background: #FEF2F2; border: 1px solid #FECACA;
border-radius: 6px; padding: 7px 10px;
}
.success-msg {
font-size: 12px; color: #15803D;
background: #F0FDF4; border: 1px solid #BBF7D0;
border-radius: 6px; padding: 7px 10px;
}
:global(html.dark .error-msg) { background: #2A1515; border-color: #3D1F1F; }
:global(html.dark .success-msg) { background: #0F2D1A; border-color: #1A4D2A; color: #4ADE80; }
.modal-footer {
display: flex; justify-content: flex-end; gap: 8px;
padding: 14px 20px;
border-top: 1px solid var(--border);
}
.btn-cancel {
padding: 7px 16px; background: var(--bg-chip);
color: var(--text-secondary); border: 1.5px solid var(--border);
border-radius: 7px; font-size: 13px; cursor: pointer;
}
.btn-cancel:hover { background: var(--bg-chip-hover); }
.btn-save {
padding: 7px 18px; background: #3B82F6;
color: #fff; border: none; border-radius: 7px;
font-size: 13px; font-weight: 600; cursor: pointer;
}
.btn-save:hover:not(:disabled) { background: #2563EB; }
.btn-save:disabled { opacity: 0.6; cursor: default; }
</style>