add docker + debug
This commit is contained in:
parent
848a79a04e
commit
1f4193f9c0
26 changed files with 21051 additions and 28 deletions
133
frontend/src/services/apiService.js
Normal file
133
frontend/src/services/apiService.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// services/apiService.js
|
||||
// Service pour gérer les appels API avec mise en cache
|
||||
|
||||
import axios from 'axios'
|
||||
import store from '@/store'
|
||||
import router from '@/router'
|
||||
import cacheService from './cacheService'
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_URL || 'http://localhost:5000/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// Intercepteur pour ajouter le token d'authentification
|
||||
apiClient.interceptors.request.use(
|
||||
config => {
|
||||
const token = store.getters.token
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => Promise.reject(error)
|
||||
)
|
||||
|
||||
// Intercepteur pour gérer les erreurs
|
||||
apiClient.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
// Déconnexion automatique si le token n'est plus valide
|
||||
store.dispatch('logout')
|
||||
router.push('/login')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default {
|
||||
// Méthodes génériques avec cache
|
||||
async get(url, params = {}, useCache = true, cacheTTL = 5 * 60 * 1000) {
|
||||
const cacheKey = `get:${url}:${JSON.stringify(params)}`
|
||||
|
||||
// Vérifier le cache si demandé
|
||||
if (useCache && cacheService.has(cacheKey)) {
|
||||
return Promise.resolve(cacheService.get(cacheKey))
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(url, { params })
|
||||
|
||||
// Mettre en cache la réponse si demandé
|
||||
if (useCache) {
|
||||
cacheService.set(cacheKey, response.data, cacheTTL)
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('API GET error:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
async post(url, data = {}, useCache = false, cacheTTL = 5 * 60 * 1000) {
|
||||
// Pour les POST, on n'utilise le cache que dans des cas spéciaux
|
||||
// indiqués par le paramètre useCache
|
||||
const cacheKey = `post:${url}:${JSON.stringify(data)}`
|
||||
|
||||
if (useCache && cacheService.has(cacheKey)) {
|
||||
return Promise.resolve(cacheService.get(cacheKey))
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(url, data)
|
||||
|
||||
if (useCache) {
|
||||
cacheService.set(cacheKey, response.data, cacheTTL)
|
||||
}
|
||||
|
||||
// Invalider le cache GET pour ce endpoint
|
||||
this.invalidateCache(url)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('API POST error:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
async put(url, data = {}) {
|
||||
try {
|
||||
const response = await apiClient.put(url, data)
|
||||
|
||||
// Invalider le cache pour ce endpoint
|
||||
this.invalidateCache(url)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('API PUT error:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
async delete(url) {
|
||||
try {
|
||||
const response = await apiClient.delete(url)
|
||||
|
||||
// Invalider le cache pour ce endpoint
|
||||
this.invalidateCache(url)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('API DELETE error:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// Méthode pour invalider le cache lié à une URL
|
||||
invalidateCache(url) {
|
||||
Object.keys(cacheService.cache).forEach(key => {
|
||||
if (key.includes(url)) {
|
||||
cacheService.invalidate(key)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// Méthode pour vider tout le cache
|
||||
clearCache() {
|
||||
cacheService.clear()
|
||||
}
|
||||
}
|
||||
69
frontend/src/services/cacheService.js
Normal file
69
frontend/src/services/cacheService.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// services/cacheService.js
|
||||
// Service pour gérer la mise en cache des données
|
||||
|
||||
class CacheService {
|
||||
constructor() {
|
||||
this.cache = {};
|
||||
this.defaultTTL = 5 * 60 * 1000; // 5 minutes en millisecondes
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une entrée en cache
|
||||
* @param {string} key - La clé de l'entrée
|
||||
* @returns {Object|null} - L'entrée ou null si non trouvée ou expirée
|
||||
*/
|
||||
get(key) {
|
||||
const item = this.cache[key];
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
// Vérifier si l'entrée est expirée
|
||||
if (Date.now() > item.expiry) {
|
||||
// Suppression automatique des entrées expirées
|
||||
delete this.cache[key];
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une entrée dans le cache
|
||||
* @param {string} key - La clé de l'entrée
|
||||
* @param {any} value - La valeur à stocker
|
||||
* @param {number} ttl - Durée de vie en millisecondes (par défaut: 5 minutes)
|
||||
*/
|
||||
set(key, value, ttl = this.defaultTTL) {
|
||||
this.cache[key] = {
|
||||
value,
|
||||
expiry: Date.now() + ttl
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une entrée existe et est valide
|
||||
* @param {string} key - La clé de l'entrée
|
||||
* @returns {boolean} - Vrai si l'entrée existe et n'est pas expirée
|
||||
*/
|
||||
has(key) {
|
||||
return this.get(key) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une entrée du cache
|
||||
* @param {string} key - La clé de l'entrée
|
||||
*/
|
||||
invalidate(key) {
|
||||
delete this.cache[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vide tout le cache
|
||||
*/
|
||||
clear() {
|
||||
this.cache = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Export d'une instance singleton
|
||||
export default new CacheService();
|
||||
|
|
@ -113,6 +113,17 @@ export default createStore({
|
|||
// Effacer les erreurs
|
||||
clearError({ commit }) {
|
||||
commit('CLEAR_ERROR')
|
||||
},
|
||||
|
||||
// Vérifier s'il existe des utilisateurs dans la base de données
|
||||
async checkUsersExist() {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/auth/check-users-exist`)
|
||||
return response.data.usersExist
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la vérification des utilisateurs', error)
|
||||
return true // Par défaut, on suppose qu'il existe des utilisateurs pour éviter des comportements inattendus
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
61
frontend/src/views/Home.vue
Normal file
61
frontend/src/views/Home.vue
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<h1>Bienvenue sur Managerr</h1>
|
||||
<p>La plateforme de gestion pour Sonarr et Radarr</p>
|
||||
<div class="actions">
|
||||
<router-link to="/login" class="btn btn-primary">Se connecter</router-link>
|
||||
<router-link to="/register" class="btn btn-secondary">Créer un compte</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Home',
|
||||
async created() {
|
||||
try {
|
||||
const usersExist = await this.$store.dispatch('checkUsersExist')
|
||||
if (!usersExist) {
|
||||
// S'il n'y a pas d'utilisateurs, rediriger directement vers la page d'inscription
|
||||
this.$router.push('/register')
|
||||
} else if (this.$store.getters.isAuthenticated) {
|
||||
// Si l'utilisateur est déjà authentifié, rediriger vers le tableau de bord
|
||||
this.$router.push('/dashboard')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la vérification des utilisateurs:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -53,7 +53,7 @@ export default {
|
|||
...mapGetters(['error', 'loading'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login', 'clearError']),
|
||||
...mapActions(['login', 'clearError', 'checkUsersExist']),
|
||||
async handleLogin() {
|
||||
try {
|
||||
await this.login({
|
||||
|
|
@ -64,10 +64,17 @@ export default {
|
|||
} catch (error) {
|
||||
// L'erreur est gérée dans le store
|
||||
}
|
||||
},
|
||||
async checkFirstConnection() {
|
||||
const usersExist = await this.checkUsersExist()
|
||||
if (!usersExist) {
|
||||
this.$router.push('/register')
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.clearError()
|
||||
this.checkFirstConnection()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
48
frontend/src/views/NotFound.vue
Normal file
48
frontend/src/views/NotFound.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div class="not-found">
|
||||
<h1>404</h1>
|
||||
<h2>Page non trouvée</h2>
|
||||
<p>La page que vous cherchez n'existe pas ou a été déplacée.</p>
|
||||
<router-link to="/" class="home-link">Retour à l'accueil</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotFound'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 6rem;
|
||||
margin: 0;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
display: inline-block;
|
||||
margin-top: 2rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
148
frontend/src/views/Register.vue
Normal file
148
frontend/src/views/Register.vue
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<div class="register">
|
||||
<h2>{{ isFirstUser ? 'Création du compte administrateur' : 'Inscription' }}</h2>
|
||||
|
||||
<div v-if="isFirstUser" class="first-user-message">
|
||||
<p>Bienvenue sur Managerr ! Aucun compte n'existe encore dans le système.</p>
|
||||
<p>Veuillez créer un premier compte qui aura les droits d'administrateur.</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="register">
|
||||
<div class="form-group">
|
||||
<label for="username">Nom d'utilisateur</label>
|
||||
<input type="text" id="username" v-model="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" v-model="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Mot de passe</label>
|
||||
<input type="password" id="password" v-model="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">Confirmer le mot de passe</label>
|
||||
<input type="password" id="confirmPassword" v-model="confirmPassword" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn-register">
|
||||
{{ isFirstUser ? 'Créer le compte administrateur' : 'S\'inscrire' }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!isFirstUser" class="login-link">Déjà un compte ? <router-link to="/login">Se connecter</router-link></p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Register',
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
isFirstUser: false
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
// Vérifier s'il s'agit du premier utilisateur
|
||||
try {
|
||||
const usersExist = await this.$store.dispatch('checkUsersExist')
|
||||
this.isFirstUser = !usersExist
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la vérification des utilisateurs:', error)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
register() {
|
||||
// Validation simple
|
||||
if (this.password !== this.confirmPassword) {
|
||||
alert("Les mots de passe ne correspondent pas");
|
||||
return;
|
||||
}
|
||||
|
||||
// Appel API pour s'inscrire
|
||||
this.$store.dispatch('register', {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
password: this.password
|
||||
}).then(() => {
|
||||
this.$router.push('/dashboard');
|
||||
}).catch(error => {
|
||||
console.error('Erreur d\'inscription:', error);
|
||||
alert("Échec de l'inscription. Veuillez réessayer.");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.register {
|
||||
max-width: 400px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-register {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.login-link a {
|
||||
color: #2196F3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.first-user-message {
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #2196F3;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.first-user-message p {
|
||||
margin: 0.5rem 0;
|
||||
color: #0d47a1;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue