Skip to content

Commit

Permalink
Added back-end mechanism for file uploads through server actions
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianLeChat committed Jan 3, 2024
1 parent 94d6089 commit 9bde590
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 62 deletions.
4 changes: 4 additions & 0 deletions app/[locale]/components/recaptcha.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export default function Recaptcha()
// On attend ensuite que les services de reCAPTCHA soient prêts.
window.grecaptcha.ready( async () =>
{
// On exécute après un événement personnalisé pour informer
// les autres composants de la disponibilité de reCAPTCHA.
window.dispatchEvent( new Event( "onRecaptchaReady" ) );

// On génère alors un jeton d'authentification...
const token = await window.grecaptcha.execute(
process.env.NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY ?? "",
Expand Down
175 changes: 175 additions & 0 deletions app/[locale]/dashboard/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//
// Actions du serveur pour le formulaire d'inscription et de connexion.
// Source : https://github.com/nextauthjs/next-auth/blob/87b037f0560c09ee186e8c0b50ae368bbff40cbe/packages/core/src/lib/pages/signin.tsx#L7-L21
//

"use server";

import mime from "mime";
import prisma from "@/utilities/prisma";
import schema from "@/schemas/file-upload";
import { auth } from "@/utilities/next-auth";
import { redirect } from "next/navigation";
import { join, parse } from "path";
import { mkdir, readdir, writeFile } from "fs/promises";
import { existsSync, statSync } from "fs";

//
// Récupération du quota de l'utilisateur.
//
export async function getUserQuota()
{
// On récupère d'abord la session de l'utilisateur.
const session = await auth();

if ( !session )
{
// Si la session n'existe pas, on redirige l'utilisateur vers
// la page d'accueil.
return redirect( "/" );
}

try
{
// On créé le dossier de stockage si celui-ci n'existe pas.
const folderPath = join( process.cwd(), "public/storage" );

await mkdir( folderPath, { recursive: true } );

// On vérifie ensuite si le dossier de l'utilisateur existe.
const userFolder = join( folderPath, session.user.id );

if ( !existsSync( userFolder ) )
{
// Si ce n'est pas le cas, le quota est de 0.
return {
success: true,
value: 0
};
}

// Dans le cas contraire, on calcule la somme des tailles des
// fichiers de l'utilisateur.
return {
success: true,
value: ( await readdir( userFolder ) ).reduce(
( previous, current ) => previous + statSync( join( userFolder, current ) ).size,
0
)
};
}
catch ( error )
{
// On affiche enfin une erreur en cas d'erreur avec le système
// de fichiers.
return {
success: false,
value: 0
};
}
}

//
// Téléversement d'un nouveau fichier.
//
export async function uploadFiles(
_state: Record<string, unknown>,
formData: FormData
)
{
// On récupère d'abord la session de l'utilisateur.
const session = await auth();

if ( !session )
{
// Si la session n'existe pas, on redirige l'utilisateur vers
// la page d'accueil.
return redirect( "/" );
}

// On tente ensuite de valider les données du formulaire.
const result = schema.safeParse( {
upload: formData.getAll( "upload" )
} );

if ( !result.success )
{
// Si les données du formulaire sont invalides, on affiche le
// premier code d'erreur rencontré.
return {
success: false,
reason: `zod.errors.${ result.error.issues[ 0 ].code }`
};
}

// On vérifie si un utilisateur existe avec l'adresse électronique
// de la session.
const user = await prisma.user.findUnique( {
where: {
email: session.user.email as string
}
} );

if ( !user )
{
// Si l'utilisateur n'existe pas, on affiche un message d'erreur
// dans le formulaire.
return {
success: false,
reason: "form.errors.unknown_user"
};
}

try
{
// On créé le dossier de l'utilisateur si celui-ci n'existe pas.
const userFolder = join(
process.cwd(),
"public/storage",
session.user.id
);

await mkdir( userFolder, { recursive: true } );

// On sort après chaque fichier téléversé de la mémoire.
result.data.upload.forEach( async ( file: File ) =>
{
// On insère le nom du fichier et son statut dans la base de
// données afin de générer un identifiant unique.
const identifier = (
await prisma.file.create( {
data: {
name: parse( file.name ).name,
userId: user.id,
status: "public"
}
} )
).fileId;

// On écrit alors le fichier dans le système de fichiers
// avec l'identifiant unique généré précédemment.
await writeFile(
join(
userFolder,
`${ identifier }.${ mime.getExtension( file.type ) }`
),
new Uint8Array( await file.arrayBuffer() )
);
} );
}
catch ( error )
{
// Si une erreur survient lors de la mise à jour de l'avatar,
// on affiche un message d'erreur générique.
return {
success: false,
reason: "form.errors.file_system"
};
}

// On retourne enfin un message de succès.
return {
success: true,
reason: "form.info.upload_success"
};
}
10 changes: 2 additions & 8 deletions app/[locale]/dashboard/components/column-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Source : https://ui.shadcn.com/docs/components/data-table#column-toggle
//
import { merge } from "@/utilities/tailwind";
import { Table } from "@tanstack/react-table";
import type { Table } from "@tanstack/react-table";
import { SlidersHorizontal } from "lucide-react";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";

Expand All @@ -14,13 +14,7 @@ import { DropdownMenu,
DropdownMenuSeparator,
DropdownMenuCheckboxItem } from "../../components/ui/dropdown-menu";

interface ColumnToggleProps<TData> {
table: Table<TData>;
}

export default function ColumnToggle<TData>( {
table
}: ColumnToggleProps<TData> )
export default function ColumnToggle<File>( { table }: { table: Table<File> } )
{
// Affichage du rendu HTML du composant.
return (
Expand Down
Loading

0 comments on commit 9bde590

Please sign in to comment.