Nom
- Identifiant / Email
+ Email
Rôle
Statut
Dernière connexion
@@ -219,25 +193,7 @@ export default function UsersAdmin() {
{user.name}
-
-
- {user.username && (
-
-
- {user.username}
-
- )}
- {user.email && (
-
-
- {user.email}
-
- )}
- {!user.username && !user.email && (
-
—
- )}
-
-
+ {user.email}
{ROLE_ICONS[user.role]}
@@ -262,20 +218,10 @@ export default function UsersAdmin() {
-
openEdit(user)}
- >
+ openEdit(user)}>
- setDeleteId(user.id)}
- >
+ setDeleteId(user.id)}>
@@ -295,65 +241,23 @@ export default function UsersAdmin() {
{editingUser ? "Modifier l'utilisateur" : "Nouvel utilisateur"}
- {editingUser
- ? "Modifiez les informations de l'utilisateur"
- : "Créez un nouveau compte. L'identifiant ou l'e-mail servira à la connexion."}
+ {editingUser ? "Modifiez les informations de l'utilisateur" : "Créez un nouveau compte utilisateur"}
- {/* Nom complet */}
- Nom complet *
- setForm((f) => ({ ...f, name: e.target.value }))}
- />
+ Nom complet
+ setForm((f) => ({ ...f, name: e.target.value }))} />
- {/* Identifiant */}
-
- Identifiant (username)
- (optionnel)
-
-
setForm((f) => ({ ...f, username: e.target.value }))}
- />
-
- Permet la connexion sans e-mail. Ex : adminItinova
-
+
Adresse e-mail
+
setForm((f) => ({ ...f, email: e.target.value }))} />
- {/* Email */}
-
- Adresse e-mail
- (optionnel)
-
- setForm((f) => ({ ...f, email: e.target.value }))}
- />
+ {editingUser ? "Nouveau mot de passe (laisser vide pour ne pas changer)" : "Mot de passe"}
+ setForm((f) => ({ ...f, password: e.target.value }))} />
- {/* Mot de passe */}
-
-
- {editingUser
- ? "Nouveau mot de passe (laisser vide pour ne pas changer)"
- : <>Mot de passe * >}
-
- setForm((f) => ({ ...f, password: e.target.value }))}
- />
-
- {/* Rôle */}
Rôle
setForm((f) => ({ ...f, role: v as Role }))}>
@@ -367,15 +271,11 @@ export default function UsersAdmin() {
- {/* Statut (modification uniquement) */}
{editingUser && (
Statut
- setForm((f) => ({ ...f, isActive: v }))}
- />
+ setForm((f) => ({ ...f, isActive: v }))} />
{form.isActive ? "Actif" : "Inactif"}
@@ -383,16 +283,9 @@ export default function UsersAdmin() {
- setShowDialog(false)}>
- Annuler
-
-
- {(createMutation.isPending || updateMutation.isPending) && (
-
- )}
+ setShowDialog(false)}>Annuler
+
+ {(createMutation.isPending || updateMutation.isPending) && }
{editingUser ? "Enregistrer" : "Créer"}
@@ -404,19 +297,11 @@ export default function UsersAdmin() {
Supprimer l'utilisateur
-
- Cette action est irréversible. L'utilisateur ne pourra plus se connecter.
-
+ Cette action est irréversible. L'utilisateur ne pourra plus se connecter.
- setDeleteId(null)}>
- Annuler
-
- deleteId && deleteMutation.mutate({ id: deleteId })}
- disabled={deleteMutation.isPending}
- >
+ setDeleteId(null)}>Annuler
+ deleteId && deleteMutation.mutate({ id: deleteId })} disabled={deleteMutation.isPending}>
{deleteMutation.isPending && }
Supprimer
diff --git a/drizzle/schema.ts b/drizzle/schema.ts
index f9c8d18..d0b2be7 100644
--- a/drizzle/schema.ts
+++ b/drizzle/schema.ts
@@ -135,3 +135,50 @@ export const ideas = mysqlTable("ideas", {
export type Idea = typeof ideas.$inferSelect;
export type InsertIdea = typeof ideas.$inferInsert;
+
+// ─── Flux RSS ────────────────────────────────────────────────────────────────────────────────────
+
+export const rssFeeds = mysqlTable("rss_feeds", {
+ id: int("id").autoincrement().primaryKey(),
+ // URL du flux RSS
+ url: text("url").notNull(),
+ // Nom descriptif du flux
+ name: varchar("name", { length: 255 }).notNull(),
+ // Type de contenu alimenté par ce flux
+ feedType: mysqlEnum("feedType", ["veille", "aap"]).notNull(),
+ // Pour les flux de type veille : type de veille par défaut
+ defaultTypeVeille: mysqlEnum("defaultTypeVeille", ["reglementaire", "concurrentielle", "technologique", "generale"]),
+ // Pour les flux de type aap : catégorie par défaut
+ defaultCategorieAap: mysqlEnum("defaultCategorieAap", ["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]),
+ // Règles d'automatisme JSON : [{keyword, typeVeille|categorieAap}]
+ autoRules: json("autoRules"),
+ // Actif ou non
+ isActive: boolean("isActive").default(true).notNull(),
+ // Dernière lecture réussie
+ lastFetchedAt: timestamp("lastFetchedAt"),
+ // Dernier statut de lecture
+ lastFetchStatus: mysqlEnum("lastFetchStatus", ["ok", "error", "pending"]).default("pending"),
+ lastFetchError: text("lastFetchError"),
+ createdAt: timestamp("createdAt").defaultNow().notNull(),
+ updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
+});
+
+export type RssFeed = typeof rssFeeds.$inferSelect;
+export type InsertRssFeed = typeof rssFeeds.$inferInsert;
+
+// Paramètres globaux RSS (fréquence de lecture, etc.)
+export const rssSettings = mysqlTable("rss_settings", {
+ id: int("id").autoincrement().primaryKey(),
+ // Fréquence de lecture en minutes (ex: 60, 360, 1440)
+ fetchIntervalMinutes: int("fetchIntervalMinutes").default(360).notNull(),
+ // Heure de lecture automatique (format HH:MM, si mode planifié)
+ scheduledTime: varchar("scheduledTime", { length: 5 }).default("06:00"),
+ // Mode : interval (toutes les N minutes) ou scheduled (heure fixe)
+ fetchMode: mysqlEnum("fetchMode", ["interval", "scheduled"]).default("scheduled").notNull(),
+ // Activer/désactiver la lecture automatique
+ autoFetchEnabled: boolean("autoFetchEnabled").default(true).notNull(),
+ updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
+});
+
+export type RssSettings = typeof rssSettings.$inferSelect;
+export type InsertRssSettings = typeof rssSettings.$inferInsert;
diff --git a/server/db.ts b/server/db.ts
index f3104e3..c93a6f6 100644
--- a/server/db.ts
+++ b/server/db.ts
@@ -11,6 +11,12 @@ import {
InsertLocalUser,
ideas,
InsertIdea,
+ rssFeeds,
+ rssSettings,
+ type InsertRssFeed,
+ type InsertRssSettings,
+ type RssFeed,
+ type RssSettings,
} from "../drizzle/schema";
import { ENV } from "./_core/env";
@@ -360,3 +366,70 @@ export async function updateIdeaStatut(id: number, statut: "ouvert" | "en_cours"
if (!db) throw new Error("Database not available");
await db.update(ideas).set({ statut }).where(eq(ideas.id, id));
}
+
+// ─── Flux RSS ────────────────────────────────────────────────────────────────────────────────────
+
+export async function getRssFeeds(): Promise {
+ const db = await getDb();
+ if (!db) return [];
+ return db.select().from(rssFeeds).orderBy(rssFeeds.name);
+}
+
+export async function getRssFeedById(id: number): Promise {
+ const db = await getDb();
+ if (!db) return null;
+ const rows = await db.select().from(rssFeeds).where(eq(rssFeeds.id, id)).limit(1);
+ return rows[0] ?? null;
+}
+
+export async function createRssFeed(data: Omit): Promise {
+ const db = await getDb();
+ if (!db) throw new Error("Database not available");
+ const result = await db.insert(rssFeeds).values(data);
+ return (result[0] as any).insertId as number;
+}
+
+export async function updateRssFeed(id: number, data: Partial>): Promise {
+ const db = await getDb();
+ if (!db) throw new Error("Database not available");
+ await db.update(rssFeeds).set(data).where(eq(rssFeeds.id, id));
+}
+
+export async function deleteRssFeed(id: number): Promise {
+ const db = await getDb();
+ if (!db) throw new Error("Database not available");
+ await db.delete(rssFeeds).where(eq(rssFeeds.id, id));
+}
+
+export async function getRssSettings(): Promise {
+ const db = await getDb();
+ if (!db) return null;
+ const rows = await db.select().from(rssSettings).limit(1);
+ if (rows.length > 0) return rows[0];
+ // Créer les paramètres par défaut si inexistants
+ await db.insert(rssSettings).values({
+ fetchIntervalMinutes: 360,
+ scheduledTime: "06:00",
+ fetchMode: "scheduled",
+ autoFetchEnabled: true,
+ });
+ const newRows = await db.select().from(rssSettings).limit(1);
+ return newRows[0] ?? null;
+}
+
+export async function saveRssSettings(data: Partial>): Promise {
+ const db = await getDb();
+ if (!db) throw new Error("Database not available");
+ const existing = await db.select().from(rssSettings).limit(1);
+ if (existing.length > 0) {
+ await db.update(rssSettings).set(data).where(eq(rssSettings.id, existing[0].id));
+ } else {
+ await db.insert(rssSettings).values({
+ fetchIntervalMinutes: 360,
+ scheduledTime: "06:00",
+ fetchMode: "scheduled",
+ autoFetchEnabled: true,
+ ...data,
+ });
+ }
+}
diff --git a/server/routers.ts b/server/routers.ts
index 2a9598c..740ba13 100644
--- a/server/routers.ts
+++ b/server/routers.ts
@@ -22,6 +22,13 @@ import {
getIdeasByUser,
repondreIdea,
updateIdeaStatut,
+ getRssFeeds,
+ getRssFeedById,
+ createRssFeed,
+ updateRssFeed,
+ deleteRssFeed,
+ getRssSettings,
+ saveRssSettings,
} from "./db";
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
@@ -50,9 +57,9 @@ export const appRouter = router({
}),
// Connexion locale
localLogin: publicProcedure
- .input(z.object({ email: z.string().min(1), password: z.string().min(1) }))
+ .input(z.object({ identifier: z.string().min(1), password: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
- const result = await loginLocalUser(input.email, input.password);
+ const result = await loginLocalUser(input.identifier, input.password);
// Stocker le token dans un cookie
const cookieOptions = getSessionCookieOptions(ctx.req);
ctx.res.cookie("veille_local_auth", result.token, {
@@ -307,6 +314,94 @@ export const appRouter = router({
return { success: true };
}),
}),
-});
+ // ─── Flux RSS ───────────────────────────────────────────────────────────────────────────────────
+ rss: router({
+ // Lister tous les flux
+ list: protectedProcedure.query(async () => {
+ return getRssFeeds();
+ }),
+ // Créer un flux
+ create: adminProcedure
+ .input(z.object({
+ url: z.string().url("URL invalide"),
+ name: z.string().min(1, "Nom requis").max(255),
+ feedType: z.enum(["veille", "aap"]),
+ defaultTypeVeille: z.enum(["reglementaire", "concurrentielle", "technologique", "generale"]).optional(),
+ defaultCategorieAap: z.enum(["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).optional(),
+ autoRules: z.array(z.object({
+ keyword: z.string(),
+ value: z.string(),
+ })).optional(),
+ isActive: z.boolean().optional().default(true),
+ }))
+ .mutation(async ({ input }) => {
+ const id = await createRssFeed({
+ url: input.url,
+ name: input.name,
+ feedType: input.feedType,
+ defaultTypeVeille: input.defaultTypeVeille ?? null,
+ defaultCategorieAap: input.defaultCategorieAap ?? null,
+ autoRules: input.autoRules ?? null,
+ isActive: input.isActive ?? true,
+ });
+ return { id };
+ }),
+
+ // Modifier un flux
+ update: adminProcedure
+ .input(z.object({
+ id: z.number().int().positive(),
+ url: z.string().url("URL invalide").optional(),
+ name: z.string().min(1).max(255).optional(),
+ feedType: z.enum(["veille", "aap"]).optional(),
+ defaultTypeVeille: z.enum(["reglementaire", "concurrentielle", "technologique", "generale"]).nullable().optional(),
+ defaultCategorieAap: z.enum(["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).nullable().optional(),
+ autoRules: z.array(z.object({
+ keyword: z.string(),
+ value: z.string(),
+ })).nullable().optional(),
+ isActive: z.boolean().optional(),
+ }))
+ .mutation(async ({ input }) => {
+ const { id, ...data } = input;
+ await updateRssFeed(id, data);
+ return { success: true };
+ }),
+
+ // Supprimer un flux
+ delete: adminProcedure
+ .input(z.object({ id: z.number().int().positive() }))
+ .mutation(async ({ input }) => {
+ await deleteRssFeed(input.id);
+ return { success: true };
+ }),
+
+ // Activer / désactiver un flux
+ toggleActive: adminProcedure
+ .input(z.object({ id: z.number().int().positive(), isActive: z.boolean() }))
+ .mutation(async ({ input }) => {
+ await updateRssFeed(input.id, { isActive: input.isActive });
+ return { success: true };
+ }),
+
+ // Lire les paramètres globaux RSS
+ getSettings: protectedProcedure.query(async () => {
+ return getRssSettings();
+ }),
+
+ // Sauvegarder les paramètres globaux RSS
+ saveSettings: adminProcedure
+ .input(z.object({
+ fetchIntervalMinutes: z.number().int().min(5).max(10080).optional(),
+ scheduledTime: z.string().regex(/^\d{2}:\d{2}$/, "Format HH:MM requis").optional(),
+ fetchMode: z.enum(["interval", "scheduled"]).optional(),
+ autoFetchEnabled: z.boolean().optional(),
+ }))
+ .mutation(async ({ input }) => {
+ await saveRssSettings(input);
+ return { success: true };
+ }),
+ }),
+});
export type AppRouter = typeof appRouter;
diff --git a/vite.config.ts.bak b/vite.config.ts.bak
new file mode 100644
index 0000000..f541271
--- /dev/null
+++ b/vite.config.ts.bak
@@ -0,0 +1,28 @@
+import { jsxLocPlugin } from "@builder.io/vite-plugin-jsx-loc";
+import react from "@vitejs/plugin-react";
+import path from "path";
+import { defineConfig } from "vite";
+import { webDevPreviewerPlugin } from "vite-plugin-web-dev-previewer";
+
+export default defineConfig({
+ plugins: [react(), jsxLocPlugin(), webDevPreviewerPlugin()],
+ resolve: {
+ alias: {
+ "@": path.resolve(import.meta.dirname, "client", "src"),
+ "@shared": path.resolve(import.meta.dirname, "shared"),
+ "@assets": path.resolve(import.meta.dirname, "attached_assets"),
+ },
+ },
+ envDir: path.resolve(import.meta.dirname),
+ root: path.resolve(import.meta.dirname, "client"),
+ build: {
+ outDir: path.resolve(import.meta.dirname, "dist/public"),
+ emptyOutDir: true,
+ },
+ server: {
+ fs: {
+ strict: true,
+ deny: ["**/.*"],
+ },
+ },
+});