feat: jira config in database
This commit is contained in:
46
clients/jira-config-client.ts
Normal file
46
clients/jira-config-client.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { httpClient } from './base/http-client';
|
||||
import { JiraConfig } from '@/lib/types';
|
||||
|
||||
export interface JiraConfigResponse {
|
||||
jiraConfig: JiraConfig;
|
||||
}
|
||||
|
||||
export interface SaveJiraConfigRequest {
|
||||
baseUrl: string;
|
||||
email: string;
|
||||
apiToken: string;
|
||||
}
|
||||
|
||||
export interface SaveJiraConfigResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
jiraConfig: JiraConfig;
|
||||
}
|
||||
|
||||
class JiraConfigClient {
|
||||
private readonly basePath = '/user-preferences/jira-config';
|
||||
|
||||
/**
|
||||
* Récupère la configuration Jira actuelle
|
||||
*/
|
||||
async getJiraConfig(): Promise<JiraConfig> {
|
||||
const response = await httpClient.get<JiraConfigResponse>(this.basePath);
|
||||
return response.jiraConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde la configuration Jira
|
||||
*/
|
||||
async saveJiraConfig(config: SaveJiraConfigRequest): Promise<SaveJiraConfigResponse> {
|
||||
return httpClient.put<SaveJiraConfigResponse>(this.basePath, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime la configuration Jira (remet à zéro)
|
||||
*/
|
||||
async deleteJiraConfig(): Promise<{ success: boolean; message: string }> {
|
||||
return httpClient.delete(this.basePath);
|
||||
}
|
||||
}
|
||||
|
||||
export const jiraConfigClient = new JiraConfigClient();
|
||||
@@ -1,46 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { AppConfig } from '@/lib/config';
|
||||
import { useJiraConfig } from '@/hooks/useJiraConfig';
|
||||
|
||||
interface JiraConfigFormProps {
|
||||
config: AppConfig;
|
||||
}
|
||||
|
||||
export function JiraConfigForm({ config }: JiraConfigFormProps) {
|
||||
export function JiraConfigForm() {
|
||||
const { config, isLoading: configLoading, saveConfig, deleteConfig } = useJiraConfig();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
apiToken: ''
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
||||
|
||||
// Charger les données existantes dans le formulaire
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setFormData({
|
||||
baseUrl: config.baseUrl || '',
|
||||
email: config.email || '',
|
||||
apiToken: config.apiToken || ''
|
||||
});
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setIsSubmitting(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
// Note: Dans un vrai environnement, ces variables seraient configurées côté serveur
|
||||
// Pour cette démo, on affiche juste un message informatif
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: 'Configuration sauvegardée. Redémarrez l'application pour appliquer les changements.'
|
||||
});
|
||||
} catch {
|
||||
const result = await saveConfig(formData);
|
||||
|
||||
if (result.success) {
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: result.message
|
||||
});
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.message
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: 'Erreur lors de la sauvegarde de la configuration'
|
||||
text: error instanceof Error ? error.message : 'Erreur lors de la sauvegarde de la configuration'
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isJiraConfigured = config.integrations.jira.enabled;
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration Jira ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const result = await deleteConfig();
|
||||
|
||||
if (result.success) {
|
||||
setFormData({
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
apiToken: ''
|
||||
});
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: result.message
|
||||
});
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.message
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: error instanceof Error ? error.message : 'Erreur lors de la suppression de la configuration'
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isJiraConfigured = config?.enabled && (config?.baseUrl || config?.email);
|
||||
const isLoading = configLoading || isSubmitting;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -67,19 +121,19 @@ export function JiraConfigForm({ config }: JiraConfigFormProps) {
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">URL de base:</span>{' '}
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
|
||||
{config.integrations.jira.baseUrl || 'Non définie'}
|
||||
{config?.baseUrl || 'Non définie'}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">Email:</span>{' '}
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
|
||||
{config.integrations.jira.email || 'Non défini'}
|
||||
{config?.email || 'Non défini'}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">Token API:</span>{' '}
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
|
||||
{config.integrations.jira.apiToken ? '••••••••' : 'Non défini'}
|
||||
{config?.apiToken ? '••••••••' : 'Non défini'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,13 +201,27 @@ export function JiraConfigForm({ config }: JiraConfigFormProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? 'Sauvegarde...' : 'Sauvegarder la configuration'}
|
||||
</Button>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
{isLoading ? 'Sauvegarde...' : 'Sauvegarder la configuration'}
|
||||
</Button>
|
||||
|
||||
{isJiraConfigured && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleDelete}
|
||||
disabled={isLoading}
|
||||
className="px-6"
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{message && (
|
||||
|
||||
@@ -6,13 +6,10 @@ import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { JiraConfigForm } from '@/components/settings/JiraConfigForm';
|
||||
import { JiraSync } from '@/components/jira/JiraSync';
|
||||
import { JiraLogs } from '@/components/jira/JiraLogs';
|
||||
import { AppConfig } from '@/lib/config';
|
||||
import { useJiraConfig } from '@/hooks/useJiraConfig';
|
||||
|
||||
interface SettingsPageClientProps {
|
||||
config: AppConfig;
|
||||
}
|
||||
|
||||
export function SettingsPageClient({ config }: SettingsPageClientProps) {
|
||||
export function SettingsPageClient() {
|
||||
const { config: jiraConfig } = useJiraConfig();
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'advanced'>('general');
|
||||
|
||||
const tabs = [
|
||||
@@ -97,14 +94,14 @@ export function SettingsPageClient({ config }: SettingsPageClientProps) {
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<JiraConfigForm config={config} />
|
||||
<JiraConfigForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Colonne 2: Actions et Logs */}
|
||||
<div className="space-y-4">
|
||||
{config.integrations.jira.enabled && (
|
||||
{jiraConfig?.enabled && (
|
||||
<>
|
||||
<JiraSync />
|
||||
<JiraLogs />
|
||||
|
||||
80
hooks/useJiraConfig.ts
Normal file
80
hooks/useJiraConfig.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { jiraConfigClient, SaveJiraConfigRequest } from '@/clients/jira-config-client';
|
||||
import { JiraConfig } from '@/lib/types';
|
||||
|
||||
export function useJiraConfig() {
|
||||
const [config, setConfig] = useState<JiraConfig | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Charger la config au montage
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const jiraConfig = await jiraConfigClient.getJiraConfig();
|
||||
setConfig(jiraConfig);
|
||||
} catch (err) {
|
||||
console.error('Erreur lors du chargement de la config Jira:', err);
|
||||
setError('Erreur lors du chargement de la configuration');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async (configData: SaveJiraConfigRequest) => {
|
||||
try {
|
||||
setError(null);
|
||||
const response = await jiraConfigClient.saveJiraConfig(configData);
|
||||
|
||||
if (response.success) {
|
||||
setConfig(response.jiraConfig);
|
||||
return { success: true, message: response.message };
|
||||
} else {
|
||||
setError('Erreur lors de la sauvegarde');
|
||||
return { success: false, message: 'Erreur lors de la sauvegarde' };
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la sauvegarde de la configuration';
|
||||
setError(errorMessage);
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
const deleteConfig = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
const response = await jiraConfigClient.deleteJiraConfig();
|
||||
|
||||
if (response.success) {
|
||||
setConfig({
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
apiToken: '',
|
||||
enabled: false
|
||||
});
|
||||
return { success: true, message: response.message };
|
||||
} else {
|
||||
setError('Erreur lors de la suppression');
|
||||
return { success: false, message: 'Erreur lors de la suppression' };
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la suppression de la configuration';
|
||||
setError(errorMessage);
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
config,
|
||||
isLoading,
|
||||
error,
|
||||
saveConfig,
|
||||
deleteConfig,
|
||||
refetch: loadConfig
|
||||
};
|
||||
}
|
||||
@@ -81,6 +81,7 @@ export interface ColumnVisibility {
|
||||
export interface JiraConfig {
|
||||
baseUrl?: string;
|
||||
email?: string;
|
||||
apiToken?: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "user_preferences" ADD COLUMN "jiraConfig" TEXT;
|
||||
@@ -98,6 +98,9 @@ model UserPreferences {
|
||||
// Visibilité des colonnes (JSON)
|
||||
columnVisibility Json?
|
||||
|
||||
// Configuration Jira (JSON)
|
||||
jiraConfig Json?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
||||
@@ -24,7 +24,10 @@ const DEFAULT_PREFERENCES: UserPreferences = {
|
||||
hiddenStatuses: []
|
||||
},
|
||||
jiraConfig: {
|
||||
enabled: false
|
||||
enabled: false,
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
apiToken: ''
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,6 +50,7 @@ class UserPreferencesService {
|
||||
kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters,
|
||||
viewPreferences: DEFAULT_PREFERENCES.viewPreferences,
|
||||
columnVisibility: DEFAULT_PREFERENCES.columnVisibility,
|
||||
jiraConfig: DEFAULT_PREFERENCES.jiraConfig as any,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -165,15 +169,43 @@ class UserPreferencesService {
|
||||
}
|
||||
}
|
||||
|
||||
// === CONFIGURATION JIRA ===
|
||||
|
||||
/**
|
||||
* Récupère la configuration Jira depuis les variables d'environnement
|
||||
* Sauvegarde la configuration Jira
|
||||
*/
|
||||
async saveJiraConfig(config: JiraConfig): Promise<void> {
|
||||
try {
|
||||
const userPrefs = await this.getOrCreateUserPreferences();
|
||||
await prisma.userPreferences.update({
|
||||
where: { id: userPrefs.id },
|
||||
data: { jiraConfig: config as any }
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Erreur lors de la sauvegarde de la config Jira:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la configuration Jira depuis la base de données avec fallback sur les variables d'environnement
|
||||
*/
|
||||
async getJiraConfig(): Promise<JiraConfig> {
|
||||
try {
|
||||
const userPrefs = await this.getOrCreateUserPreferences();
|
||||
const dbConfig = (userPrefs as any).jiraConfig as JiraConfig | null;
|
||||
|
||||
// Si config en DB, l'utiliser
|
||||
if (dbConfig && (dbConfig.baseUrl || dbConfig.email || dbConfig.apiToken)) {
|
||||
return { ...DEFAULT_PREFERENCES.jiraConfig, ...dbConfig };
|
||||
}
|
||||
|
||||
// Sinon fallback sur les variables d'environnement (existant)
|
||||
const config = getConfig();
|
||||
return {
|
||||
baseUrl: config.integrations.jira.baseUrl,
|
||||
email: config.integrations.jira.email,
|
||||
apiToken: '', // On ne retourne pas le token des env vars pour la sécurité
|
||||
enabled: config.integrations.jira.enabled
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -202,14 +234,14 @@ class UserPreferencesService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde toutes les préférences utilisateur (jiraConfig ignorée car elle vient des env vars)
|
||||
* Sauvegarde toutes les préférences utilisateur
|
||||
*/
|
||||
async saveAllPreferences(preferences: UserPreferences): Promise<void> {
|
||||
await Promise.all([
|
||||
this.saveKanbanFilters(preferences.kanbanFilters),
|
||||
this.saveViewPreferences(preferences.viewPreferences),
|
||||
this.saveColumnVisibility(preferences.columnVisibility)
|
||||
// jiraConfig n'est pas sauvegardée car elle vient des variables d'environnement
|
||||
this.saveColumnVisibility(preferences.columnVisibility),
|
||||
this.saveJiraConfig(preferences.jiraConfig)
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -225,6 +257,7 @@ class UserPreferencesService {
|
||||
kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters,
|
||||
viewPreferences: DEFAULT_PREFERENCES.viewPreferences,
|
||||
columnVisibility: DEFAULT_PREFERENCES.columnVisibility,
|
||||
jiraConfig: DEFAULT_PREFERENCES.jiraConfig as any,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
110
src/app/api/user-preferences/jira-config/route.ts
Normal file
110
src/app/api/user-preferences/jira-config/route.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { userPreferencesService } from '@/services/user-preferences';
|
||||
import { JiraConfig } from '@/lib/types';
|
||||
|
||||
/**
|
||||
* GET /api/user-preferences/jira-config
|
||||
* Récupère la configuration Jira
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
const jiraConfig = await userPreferencesService.getJiraConfig();
|
||||
return NextResponse.json({ jiraConfig });
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération de la config Jira:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erreur lors de la récupération de la configuration Jira' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/user-preferences/jira-config
|
||||
* Sauvegarde la configuration Jira
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { baseUrl, email, apiToken } = body;
|
||||
|
||||
// Validation des données requises
|
||||
if (!baseUrl || !email || !apiToken) {
|
||||
return NextResponse.json(
|
||||
{ error: 'baseUrl, email et apiToken sont requis' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validation du format URL
|
||||
try {
|
||||
new URL(baseUrl);
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'baseUrl doit être une URL valide' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validation du format email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'email doit être un email valide' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const jiraConfig: JiraConfig = {
|
||||
baseUrl: baseUrl.trim(),
|
||||
email: email.trim(),
|
||||
apiToken: apiToken.trim(),
|
||||
enabled: true
|
||||
};
|
||||
|
||||
await userPreferencesService.saveJiraConfig(jiraConfig);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Configuration Jira sauvegardée avec succès',
|
||||
jiraConfig: {
|
||||
...jiraConfig,
|
||||
apiToken: '••••••••' // Masquer le token dans la réponse
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde de la config Jira:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erreur lors de la sauvegarde de la configuration Jira' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/user-preferences/jira-config
|
||||
* Supprime la configuration Jira (remet à zéro)
|
||||
*/
|
||||
export async function DELETE() {
|
||||
try {
|
||||
const defaultConfig: JiraConfig = {
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
apiToken: '',
|
||||
enabled: false
|
||||
};
|
||||
|
||||
await userPreferencesService.saveJiraConfig(defaultConfig);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Configuration Jira réinitialisée avec succès'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression de la config Jira:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erreur lors de la suppression de la configuration Jira' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,8 @@
|
||||
import { SettingsPageClient } from '@/components/settings/SettingsPageClient';
|
||||
import { getConfig } from '@/lib/config';
|
||||
|
||||
// Force dynamic rendering (no static generation)
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const config = getConfig();
|
||||
|
||||
return (
|
||||
<SettingsPageClient
|
||||
config={config}
|
||||
/>
|
||||
);
|
||||
return <SettingsPageClient />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user