Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,22 @@ const app = new Elysia()
})
.get("/uploads/:filename", async ({ params, set }) => {
const filename = basename(params.filename);
if (filename !== params.filename || !filename.toLowerCase().endsWith(".pdf")) {
const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
const CONTENT_TYPES: Record<string, string> = {
".pdf": "application/pdf",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".md": "text/markdown",
};
if (filename !== params.filename || !CONTENT_TYPES[ext]) {
set.status = 400;
return {message: "Invalid filename"}
return { message: "Invalid filename" };
}
try {
const buf = await readFile(join(UPLOADS_DIR, filename));
return new Response(buf, {
headers: { "Content-Type": "application/pdf", "Content-Disposition": `inline; filename="${filename}"` },
headers: { "Content-Type": CONTENT_TYPES[ext], "Content-Disposition": `inline; filename="${filename}"` },
});
} catch {
set.status = 404;
Expand Down
15 changes: 9 additions & 6 deletions backend/src/services/subject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ const DIFFICULTY_LABEL: Record<Difficulty, string> = {

const UPLOADS_DIR = process.env.UPLOADS_DIR ?? join(import.meta.dir, "../../uploads");

async function savePdfs(files: File[]): Promise<string[]> {
const ALLOWED_EXTENSIONS = new Set([".pdf", ".png", ".jpg", ".jpeg", ".md"]);

async function saveFiles(files: File[]): Promise<string[]> {
const saved: string[] = [];
for (const f of files) {
const fileName = f.name.replace(/[\\/]/g, "_");
if (!fileName.toLowerCase().endsWith(".pdf"))
throw new Error("Only PDF files are allowed");
const ext = fileName.slice(fileName.lastIndexOf(".")).toLowerCase();
if (!ALLOWED_EXTENSIONS.has(ext))
throw new Error(`Type de fichier non autorisé : ${ext}`);
const buffer = Buffer.from(await f.arrayBuffer());
await writeFile(join(UPLOADS_DIR, fileName), buffer);
saved.push(fileName);
Expand All @@ -38,7 +41,7 @@ export const subjectService = {

const tags = data.tags.split(",").map(t => t.trim()).filter(Boolean);
const urls = (data.urls ?? "").split("\n").map(u => u.trim()).filter(Boolean);
const files = await savePdfs(data.newFiles ?? []);
const files = await saveFiles(data.newFiles ?? []);

return subjectModel.create({ name: data.name, description: data.description, difficulty, tags, files, urls });
},
Expand All @@ -59,7 +62,7 @@ export const subjectService = {

const tags = (data.tags ?? "").split(",").map(t => t.trim()).filter(Boolean);
const urls = (data.urls ?? "").split("\n").map(u => u.trim()).filter(Boolean);
const files = await savePdfs(data.newFiles ?? []);
const files = await saveFiles(data.newFiles ?? []);

return subjectModel.create({
name: data.name,
Expand Down Expand Up @@ -94,7 +97,7 @@ export const subjectService = {

const tags = data.tags.split(",").map(t => t.trim()).filter(Boolean);
const urls = (data.urls ?? "").split("\n").map(u => u.trim()).filter(Boolean);
const newFileNames = await savePdfs(data.newFiles ?? []);
const newFileNames = await saveFiles(data.newFiles ?? []);
const files = [...(data.existingFiles ?? []), ...newFileNames];

return subjectModel.update(id, { name: data.name, description: data.description, difficulty, tags, files, urls });
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const server = serve({
const publicUrl = process.env.PUBLIC_URL ?? server.url.toString();
console.log(`
┌─────────────────────────────────────────┐
│ GCC Dev Environment │
GCC Dev Environment │
├─────────────────────────────────────────┤
│ Frontend → ${publicUrl.padEnd(25)}│
│ Backend → http://localhost:8080 │
Expand Down
30 changes: 19 additions & 11 deletions frontend/src/pages/AdminPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,14 @@ function SubjectForm({ token }: { token: string }) {
const [error, setError] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null);

const ALLOWED_EXTS = new Set([".pdf", ".png", ".jpg", ".jpeg", ".md"]);
const addFiles = (list: FileList | null) => {
if (!list) return;
const pdfs = Array.from(list).filter(f => f.type === "application/pdf");
setPdfFiles(prev => [...prev, ...pdfs]);
const valid = Array.from(list).filter(f => {
const ext = f.name.slice(f.name.lastIndexOf(".")).toLowerCase();
return ALLOWED_EXTS.has(ext);
});
setPdfFiles(prev => [...prev, ...valid]);
};

const reset = () => {
Expand Down Expand Up @@ -402,7 +406,7 @@ function SubjectForm({ token }: { token: string }) {
</div>

<div>
<Text size="sm" fw={600} mb={8}>Fichiers PDF <Text component="span" size="xs" c="dimmed">(optionnel)</Text></Text>
<Text size="sm" fw={600} mb={8}>Fichiers <Text component="span" size="xs" c="dimmed">(PDF, image, Markdown (optionnel))</Text></Text>
<Stack gap={8}>
{pdfFiles.map((f, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", background: "var(--epi-bg)", border: "1px solid var(--epi-beginner)", borderRadius: 8, padding: "10px 14px" }}>
Expand All @@ -429,11 +433,11 @@ function SubjectForm({ token }: { token: string }) {
>
<Stack gap={6} align="center">
<IconUpload size={22} color="var(--epi-ghost)" />
<Text size="sm" c="dimmed">Glisse des PDFs ici ou <span style={{ color: "var(--epi-accent)", fontWeight: 600 }}>clique pour choisir</span></Text>
<Text size="xs" c="dimmed">PDF uniquement (plusieurs fichiers acceptés)</Text>
<Text size="sm" c="dimmed">Glisse des fichiers ici ou <span style={{ color: "var(--epi-accent)", fontWeight: 600 }}>clique pour choisir</span></Text>
<Text size="xs" c="dimmed">PDF, PNG, JPG, Markdown (plusieurs fichiers acceptés)</Text>
</Stack>
</div>
<input ref={fileInputRef} type="file" accept="application/pdf" multiple style={{ display: "none" }}
<input ref={fileInputRef} type="file" accept=".pdf,.png,.jpg,.jpeg,.md" multiple style={{ display: "none" }}
onChange={e => { addFiles(e.target.files); if (fileInputRef.current) fileInputRef.current.value = ""; }} />
</Stack>
</div>
Expand Down Expand Up @@ -494,10 +498,14 @@ function EditModal({ subject, token, onClose, onSaved }: {
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");

const ALLOWED_EXTS = new Set([".pdf", ".png", ".jpg", ".jpeg", ".md"]);
const addFiles = (list: FileList | null) => {
if (!list) return;
const pdfs = Array.from(list).filter(f => f.type === "application/pdf");
setNewPdfFiles(prev => [...prev, ...pdfs]);
const valid = Array.from(list).filter(f => {
const ext = f.name.slice(f.name.lastIndexOf(".")).toLowerCase();
return ALLOWED_EXTS.has(ext);
});
setNewPdfFiles(prev => [...prev, ...valid]);
};

const submit = async (e: { preventDefault: () => void }) => {
Expand Down Expand Up @@ -618,7 +626,7 @@ function EditModal({ subject, token, onClose, onSaved }: {
</div>

<div>
<Text size="sm" fw={600} mb={8}>Fichiers PDF <Text component="span" size="xs" c="dimmed">(optionnel)</Text></Text>
<Text size="sm" fw={600} mb={8}>Fichiers <Text component="span" size="xs" c="dimmed">(PDF, image, Markdown (optionnel))</Text></Text>
<Stack gap={8}>
{keepFiles.map(name => (
<div key={name} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", background: "var(--epi-bg)", border: "1px solid var(--epi-border)", borderRadius: 8, padding: "10px 14px" }}>
Expand Down Expand Up @@ -651,10 +659,10 @@ function EditModal({ subject, token, onClose, onSaved }: {
style={{ border: `2px dashed ${dragOver ? "var(--epi-accent)" : "var(--epi-border)"}`, borderRadius: 10, padding: "20px", textAlign: "center", cursor: "pointer", transition: "border-color 0.2s", background: dragOver ? "rgba(128,157,253,0.05)" : "none" }}>
<Group justify="center" gap="xs">
<IconUpload size={16} color="var(--epi-ghost)" />
<Text size="sm" c="dimmed">Glisse des PDFs ou <span style={{ color: "var(--epi-accent)", fontWeight: 600 }}>clique</span></Text>
<Text size="sm" c="dimmed">Glisse des fichiers ou <span style={{ color: "var(--epi-accent)", fontWeight: 600 }}>clique</span></Text>
</Group>
</div>
<input ref={editFileRef} type="file" accept="application/pdf" multiple style={{ display: "none" }}
<input ref={editFileRef} type="file" accept=".pdf,.png,.jpg,.jpeg,.md" multiple style={{ display: "none" }}
onChange={e => { addFiles(e.target.files); if (editFileRef.current) editFileRef.current.value = ""; }} />
</Stack>
</div>
Expand Down
Loading