Loading...
Building saas part4
Author Cloudapp
E.G.

Next.js 14 — Building a SaaS Solution on Azure (Storage Accounts etc.) — Part 4

November 6, 2024
Table of Contents

In this final part of our series on building a SaaS solution using Next.js 14 and Azure, we will focus on the critical aspect of managing Azure Storage Accounts. We’ll dive into how you can create storage accounts, handle file uploads, monitor disk usage, and delete files using APIs.

Github Repo and Example Page with Final Result

More details and the GitHub repo with the entire code

All needed information for creating the used App Service can be found here:

https://www.cloudapp.dev/next-js-14-building-a-saas-solution-on-azure-long-running-processes-part-1

Setting Up Storage Accounts via API

We begin by creating an Azure Storage Account. The API route
/api/azure/resources/createstorageaccountappservice
in our project is responsible for interacting with Azure App Services to create the storage account securely.

Here’s how the route looks:

import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";

export const dynamic = "force-dynamic";

export async function POST(req: NextRequest) {
  const authtoken = await getToken({ req });
  const isAdmin = authtoken?.role === "Admin";
  if (!authtoken || !isAdmin) {
    return NextResponse.json(
      { error: "Unauthorized Create Storage API" },
      { status: 401 }
    );
  }

  const accessToken = authtoken?.accessToken;
  if (!accessToken) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  try {
    const {
      resourceGroupName,
      accountName,
      location,
      containerName,
      user_email,
      user_az_id,
      tags,
    } = await req.json();

    const payload = {
      resourceGroupName,
      accountName,
      location,
      tags,
      containerName,
      user_email,
      user_az_id,
    };

    const url = `https://${process.env.AZURE_APP_SERVICE_NAME}.azurewebsites.net/api/storage/create-storage-account`;

    const response = await fetch(url, {
      method: "POST",
      body: JSON.stringify(payload),
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
    });

    if (!response.ok) {
      throw new Error(`HTTP error ServiceBus! status: ${response.status}`);
    }

    const responseText = await response.text();
    return NextResponse.json({ message: responseText ? "Message sent successfully" : "Message sent successfully via Text" });

  } catch (err) {
    return NextResponse.json({ error: "Invalid request" }, { status: 400 });
  }
}

This route ensures only authorized users (Admins) can create storage accounts by validating the user’s role. Once authenticated, it sends the storage account creation request to an Azure App Service.

Deleting Azure Resource Groups via API with Admin Authorization

In this part of the SaaS solution, we implement an API route for deleting resource groups based on tags, but only for authorized users. The POST method in the following code ensures that only users with the Admin role can execute the deletion. Path -> /api/azure/resources/deleteresourcegroupappservice

After obtaining the authorization token, the payload, including tagKey, tagValue, and resourceGroupName, is constructed and sent to Azure’s App Service using a DELETE request.

Here’s the full code:

import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";

export const dynamic = "force-dynamic";

export async function POST(req: NextRequest) {
  const authtoken = await getToken({ req });
  const isAdmin = authtoken?.role === "Admin";
  if (!authtoken || !isAdmin) {
    return NextResponse.json(
      { error: "Unauthorized Create Storage API" },
      { status: 401 }
    );
  }

  // Respond with a success message
  const accessToken = authtoken?.accessToken;

  if (!accessToken) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  try {
    const { tagKey, tagValue, resourceGroupName, user_email } =
      await req.json();

    // Payload to send
    const payload: any = {
      tagKey,
      tagValue,
      resourceGroupName,
      user_email,
    };
    // Convert the payload to a JSON string
    const message = payload;

    if (!message) {
      return NextResponse.json(
        { error: "Message body is required." },
        { status: 400 }
      );
    }

    // Rest API URL Service Bus
    const url = `https://${process.env.AZURE_APP_SERVICE_NAME}.azurewebsites.net/api/storage/delete-resource-groups-by-tag`;

    try {
      const response = await fetch(url, {
        method: "DELETE",
        body: JSON.stringify(payload),
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json",
        },
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      // Check if the response body exists and is not empty
      const responseText = await response.text();
      if (responseText) {
        return NextResponse.json({ message: "Message sent successfully" });
        // const data = JSON.parse(responseText);
        // return data;
      } else {
        // console.warn("No response body received");
        return NextResponse.json({ message: responseText });
        // return NextResponse.json({ message: "Message sent successfully" });
      }
    } catch (err) {
      console.error("Error sending message to App Service:", err);
      return NextResponse.json(
        { error: "Error sending message to App Service" },
        { status: 500 }
      );
    }
  } catch (err) {
    console.error("Error processing request:", err);
    return NextResponse.json({ error: "Invalid request" }, { status: 400 });
  }
}

How It Works:

  1. Authorization:

    It validates if the user has an Admin role using the JWT token from next-auth/jwt.

  2. Payload Construction:

    The API builds a payload that contains the necessary data like tagKey, tagValue, resourceGroupName, and user_email.

  3. DELETE Request:

    It sends this payload via a DELETE request to an Azure App Service endpoint, where it deletes resource groups based on the provided tags.

  4. Error Handling:

    The code handles errors such as unauthorized access or issues during the communication with the Azure Service, ensuring the API behaves robustly.

This implementation ensures that critical resources like Azure Resource Groups can only be deleted by authorized users in your SaaS application.

Uploading Files with the Frontend Component

Next, we focus on how to handle file uploads to the storage account. The FileUploader component provides users with a UI to drag and drop files, upload them, and monitor their progress.

Here is the complete implementation of the FileUploader component:

 "use client";

import { useState, useEffect, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { Doughnut } from "react-chartjs-2";
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";

ChartJS.register(ArcElement, Tooltip, Legend);

// Helper function to format file sizes in MB
const formatFileSizeMB = (size: number) => {
  return `${(size / (1024 * 1024)).toFixed(2)} MB`;
};

// Helper function to format dates in a readable format
const formatDate = (dateString: string) => {
  const date = new Date(dateString);
  return date.toLocaleDateString() + " " + date.toLocaleTimeString();
};

interface FileWithProgress {
  file: File;
  progress: number;
  uploaded: boolean;
}

interface UploadedFile {
  name: string;
  size: number;
  url: string;
  createdAt: string; // Added createdAt for displaying creation date
}

const MAX_DISK_SPACE_MB = 5 * 1024; // 5 GB in MB

interface FileUploaderProps {
  storageAccountName: string; // Prop to control loading state
  accessKey: string; // Prop to set the button label when not loading
  containerName?: string; // Prop to set the button label when loading is finished
}

const FileUploader: React.FC<FileUploaderProps> = ({
  storageAccountName,
  accessKey,
  containerName,
}) => {
  const [files, setFiles] = useState<FileWithProgress[]>([]);
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
  // const [uploadedFiles, setUploadedFiles] = useState<
  //   { name: string; size: number; url: string }[]
  // >([]);

  // console.log("storageaccountname", storageAccountName);
  // console.log("accessKey", accessKey);
  // console.log("containername", containerName);

  const [totalDiskSpace, setTotalDiskSpace] = useState<number>(0);

  // Fetch the list of uploaded files from the server and calculate total disk space
  const fetchUploadedFiles = useCallback(async () => {
    const response = await fetch("/api/azure/storageaccount/list", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        storageAccountName: storageAccountName || "",
        accessKey: accessKey || "",
        containerName: containerName || "",
      }),
    });

    const data = await response.json();
    setUploadedFiles(data.files);

    const totalSize = data.files.reduce(
      (acc: number, file: { size: number }) => acc + file.size,
      0
    );
    setTotalDiskSpace(totalSize / (1024 * 1024)); // Convert total size to MB
  }, [storageAccountName, accessKey, containerName]);

  // Clear Uploaded File List
  const clearuploadedFileList = () => {
    setFiles([]);
  };

  // Handle file drop
  const onDrop = useCallback((acceptedFiles: File[]) => {
    const newFiles = acceptedFiles.map((file) => ({
      file,
      progress: 0,
      uploaded: false,
    }));
    setFiles((prevFiles) => [...prevFiles, ...newFiles]);
  }, []);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    multiple: true,
  });

  const uploadFileToAzure = async (file: File, index: number) => {
    const updatedFiles = [...files];

    // Step 1: Request SAS token from the API
    try {
      const response = await fetch("/api/azure/storageaccount/sastoken", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          fileName: file.name,
          storageAccountName: storageAccountName,
          accessKey: accessKey,
          containerName: containerName,
        }),
      });

      const { uploadUrl, blobUrl } = await response.json();

      // Step 2: Upload the file directly to Azure Blob Storage using the SAS URL
      const xhr = new XMLHttpRequest();
      xhr.open("PUT", uploadUrl, true);
      xhr.setRequestHeader("x-ms-blob-type", "BlockBlob");
      xhr.setRequestHeader("Content-Type", file.type);

      xhr.upload.onprogress = (event) => {
        if (event.lengthComputable) {
          const percentComplete = (event.loaded / event.total) * 100;
          updatedFiles[index].progress = percentComplete;
          setFiles([...updatedFiles]);
        }
      };

      xhr.onload = () => {
        if (xhr.status === 201) {
          // console.log(`File ${file.name} uploaded successfully!`);
          setFiles((prevFiles) => {
            const updatedFiles = [...prevFiles];
            updatedFiles[index].uploaded = true;
            return updatedFiles;
          });
          fetchUploadedFiles(); // Refresh the uploaded files list
        } else {
          console.error(`Error uploading file ${file.name}`);
        }
      };

      xhr.onerror = () => {
        console.error(`File ${file.name} upload failed due to a network error`);
      };

      await new Promise((resolve) => {
        xhr.onloadend = resolve;
        xhr.send(file);
      });
    } catch (error) {
      console.error(`Failed to upload file ${file.name}:`, error);
    }
  };

  const handleUpload = () => {
    files.forEach((fileWithProgress, index) => {
      if (!fileWithProgress.uploaded) {
        uploadFileToAzure(fileWithProgress.file, index);
      }
    });
  };

  // Handle file deletion before upload
  const handleDeleteBeforeUpload = (index: number) => {
    const updatedFiles = [...files];
    updatedFiles.splice(index, 1);
    setFiles(updatedFiles);
  };

  // Handle file deletion after upload
  const handleDeleteAfterUpload = async (fileName: string) => {
    try {
      const response = await fetch(
        `/api/azure/storageaccount/filedelete?fileName=${encodeURIComponent(
          fileName
        )}`,
        {
          method: "DELETE",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            storageAccountName: storageAccountName,
            accessKey: accessKey,
            containerName: containerName,
          }),
        }
      );

      if (response.ok) {
        // console.log(`File ${fileName} deleted successfully`);
        fetchUploadedFiles(); // Refresh the uploaded files list after deletion
      } else {
        console.error(`Failed to delete file ${fileName}`);
      }
    } catch (error) {
      console.error(`Error deleting file ${fileName}:`, error);
    }
  };

  // Handle deletion of all files
  const handleDeleteAll = async () => {
    try {
      const response = await fetch("/api/azure/storageaccount/deleteall", {
        method: "DELETE",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          storageAccountName: storageAccountName,
          accessKey: accessKey,
          containerName: containerName,
        }),
      });

      if (response.ok) {
        // console.log("All files deleted successfully");
        fetchUploadedFiles(); // Refresh the uploaded files list after deletion
      } else {
        console.error("Failed to delete all files");
      }
    } catch (error) {
      console.error("Error deleting all files:", error);
    }
  };

  // Fetch files on component mount
  useEffect(() => {
    fetchUploadedFiles();
  }, [fetchUploadedFiles]);

  // Data for the donut chart
  const donutChartData = {
    labels: ["Used Space", "Free Space"],
    datasets: [
      {
        data: [totalDiskSpace, MAX_DISK_SPACE_MB - totalDiskSpace],
        backgroundColor: ["#3b82f6", "#e5e7eb"], // Blue for used space, grey for free space
        hoverBackgroundColor: ["#2563eb", "#d1d5db"],
        borderWidth: 1,
      },
    ],
  };

  // Options for the donut chart to place text in the middle
  const donutChartOptions: any = {
    cutout: "70%", // Creates space in the center of the donut
    plugins: {
      tooltip: {
        callbacks: {
          label: function (tooltipItem: any) {
            return `${tooltipItem.label}: ${tooltipItem.raw.toFixed(2)} MB`;
          },
        },
      },
    },
    elements: {
      center: {
        text: `${totalDiskSpace.toFixed(2)} MB / ${MAX_DISK_SPACE_MB.toFixed(
          2
        )} MB`,
        color: "#333", // Center text color
        fontStyle: "Arial", // Center text font style
        sidePadding: 5, // Padding around the center text
      },
    },
  };

  return (
    <div className="max-w-2xl mx-auto mt-8 dark:bg-gray-800 p-6 rounded-lg shadow-md">
      <h1 className="text-3xl font-bold text-center mb-8">
        Multi-File Upload to Azure Storage
        {totalDiskSpace > 0 && (
          <span className="text-lg font-normal">
            {" "}
            ({formatFileSizeMB(totalDiskSpace * 1024 * 1024)} used)
          </span>
        )}
      </h1>

      {/* Donut Chart for Disk Space */}
      <div className="relative mb-8 flex justify-center">
        <div className="h-64 w-64">
          {/* Adjust these values to control the size */}
          <Doughnut data={donutChartData} options={donutChartOptions} />
        </div>

        <div className="absolute inset-0 flex flex-col items-center justify-center">
          <span className="text-lg font-bold dark:text-white text-gray-800">
            {totalDiskSpace.toFixed(2)} MB
          </span>
          <span className="text-sm text-gray-500">
            of {MAX_DISK_SPACE_MB.toFixed(2)} MB
          </span>
        </div>
      </div>

      {/* File Upload Section with Dropzone */}
      <div>
        <h2 className="text-xl font-bold mb-4">Upload Files</h2>
        <div
          {...getRootProps()}
          className={`p-6 border-2 border-dashed rounded-lg cursor-pointer ${
            isDragActive ? "border-blue-500" : "border-gray-300"
          }`}
        >
          <input {...getInputProps()} />
          {isDragActive ? (
            <p className="text-center text-gray-600">Drop the files here...</p>
          ) : (
            <p className="text-center text-gray-600">
              Drag & drop files here, or click to select files
            </p>
          )}
        </div>
        <button
          onClick={handleUpload}
          className={`mt-4 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition ${
            files.length === 0 ? "opacity-50 cursor-not-allowed" : ""
          }`}
          disabled={files.length === 0}
        >
          {files.length === 0 ? "Select files to upload" : "Upload All Files"}
        </button>

        {/* Display Selected Files with Progress and Delete Option */}
        {files.length > 0 && (
          <div className="mt-4">
            <h3 className="text-lg font-bold mb-2">Selected Files</h3>
            <ul className="space-y-2">
              {files.map((fileWithProgress, index) => (
                <li key={index} className="flex items-center space-x-4">
                  {/* File name */}
                  <span className="dark:text-white text-gray-700 w-1/3 break-words">
                    {fileWithProgress.file.name}
                  </span>

                  {/* Progress bar with percentage */}
                  <div className="flex items-center w-1/2">
                    <div className="relative w-full bg-gray-200 rounded-full h-4">
                      <div
                        className="bg-blue-600 h-4 rounded-full"
                        style={{ width: `${fileWithProgress.progress}%` }}
                      ></div>
                      <span className="absolute inset-0 flex justify-center items-center text-white text-sm">
                        {fileWithProgress.progress.toFixed(0)}%
                      </span>
                    </div>
                  </div>

                  {/* Delete button */}
                  <button
                    onClick={() => handleDeleteBeforeUpload(index)}
                    className="text-red-600 hover:text-red-800"
                  >
                    Delete
                  </button>
                </li>
              ))}
            </ul>
          </div>
        )}
      </div>

      {/* Uploaded Files Section with Delete Option */}
      <div className="mt-8">
        <h2 className="text-xl font-bold mb-4">Uploaded Files</h2>
        {uploadedFiles && uploadedFiles.length > 0 && (
          <>
            <button
              onClick={handleDeleteAll}
              className="mb-4 bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition"
            >
              Delete All Files
            </button>
            <button
              onClick={fetchUploadedFiles}
              className="ml-4 mb-4 bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition"
            >
              Refresh File List
            </button>
            <button
              onClick={clearuploadedFileList}
              className="ml-4 mb-4 bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition"
            >
              Clear Uploaded File List
            </button>
            <ul className="list-disc  pl-6 space-y-2">
              {uploadedFiles.map((file, index) => (
                <li
                  key={index}
                  className="flex items-center justify-between dark:text-white text-gray-700"
                >
                  <span>
                    {file.name} - {formatFileSizeMB(file.size)}
                  </span>
                  <br />
                  <span>Created At: {formatDate(file.createdAt)}</span>{" "}
                  <button
                    onClick={() => handleDeleteAfterUpload(file.name)}
                    className="text-red-600 hover:text-red-800"
                  >
                    Delete
                  </button>
                </li>
              ))}
            </ul>
          </>
        )}
      </div>
    </div>
  );
};

export default FileUploader;

Deleting Files and Storage Management

The FileUploader component also includes the ability to delete files and even entire storage containers, allowing for efficient storage management.

Below is the API route for deleting all files in a storage container:

import { NextRequest, NextResponse } from "next/server";
import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob";
import { getToken } from "next-auth/jwt";

export async function DELETE(req: NextRequest) {
  const token = await getToken({ req });

  if (!token) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { storageAccountName, accessKey, containerName } = await req.json();
  const blobServiceClient = new BlobServiceClient(
    `https://${storageAccountName}.blob.core.windows.net`,
    new StorageSharedKeyCredential(storageAccountName, accessKey)
  );

  try {
    const containerClient = blobServiceClient.getContainerClient(containerName);
    for await (const blob of containerClient.listBlobsFlat()) {
      const blockBlobClient = containerClient.getBlockBlobClient(blob.name);
      await blockBlobClient.deleteIfExists();
    }
    return NextResponse.json({ message: "All files deleted successfully" });
  } catch (error) {
    return NextResponse.json({ error: "Failed to delete all files" }, { status: 500 });
  }
}

This route ensures that only authorized users can delete files. The BlobServiceClient handles interactions with Azure Blob Storage, iterating through all files and deleting them.

Uploading Files via SAS Token

In addition to creating and deleting storage accounts, users can also upload files to Azure Blob Storage via SAS Tokens. Below is the code for generating a SAS token:

import { BlobServiceClient, StorageSharedKeyCredential, generateBlobSASQueryParameters, ContainerSASPermissions } from "@azure/storage-blob";
import { NextResponse, NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";

export async function POST(req: NextRequest) {
  const token = await getToken({ req });
  const { storageAccountName, accessKey, containerName, fileName } = await req.json();

  if (!token) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const blobServiceClient = new BlobServiceClient(
    `https://${storageAccountName}.blob.core.windows.net`,
    new StorageSharedKeyCredential(storageAccountName, accessKey)
  );

  const containerClient = blobServiceClient.getContainerClient(containerName);
  const blobClient = containerClient.getBlockBlobClient(fileName);

  const sasToken = generateBlobSASQueryParameters(
    {
      containerName,
      blobName: fileName,
      permissions: ContainerSASPermissions.parse("cwr"),
      startsOn: new Date(),
      expiresOn: new Date(new Date().valueOf() + 3600 * 1000), // 1 hour
    },
    blobServiceClient.credential as StorageSharedKeyCredential
  ).toString();

  return NextResponse.json({
    uploadUrl: `${blobClient.url}?${sasToken}`,
    blobUrl: blobClient.url,
  });
}

This API route generates a SAS Token that allows secure, time-limited uploads directly to Azure Blob Storage. The ContainerSASPermissions ensures that only the necessary permissions are granted (in this case, create, write, and read).

If you need to delete specific files rather than clearing an entire container, the following API route can delete individual files based on search criteria:

import { NextRequest, NextResponse } from "next/server";
import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob";
import { getToken } from "next-auth/jwt";

export async function DELETE(req: NextRequest) {
  const token = await getToken({ req });
  const { storageAccountName, accessKey, containerName } = await req.json();

  const { searchParams } = new URL(req.url);
  const fileName = searchParams.get("fileName");

  if (!token || !fileName) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const blobServiceClient = new BlobServiceClient(
    `https://${storageAccountName}.blob.core.windows.net`,
    new StorageSharedKeyCredential(storageAccountName, accessKey)
  );

  const containerClient = blobServiceClient.getContainerClient(containerName);
  const blockBlobClient = containerClient.getBlockBlobClient(fileName);

  try {
    await blockBlobClient.deleteIfExists();
    return NextResponse.json({ message: `File ${fileName} deleted successfully` });
  } catch (error) {
    return NextResponse.json({ error: `Failed to delete file ${fileName}` }, { status: 500 });
  }
}

This route leverages search parameters from the request URL to locate and delete individual files from Azure Blob Storage.

Main Component (createstorageaccountform_appservice.component.tsx)

In this component, we are managing the creation and deletion of Azure Storage accounts and embedding the FileUploader component to handle file uploads within those accounts.

"use client";
import React, { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import LoadingButton from "@/components/tools/loadingbutton/loadingbutton.component";
import FileUploader from "@/components/azure/storageaccounts/fileuploader.component";

// Utility function to generate a random alphanumeric string of a specified length
const generateRandomName = (length: number) => {
  const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
  let result = "";
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * characters.length));
  }
  return result;
};

// const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const CreateStorageAccountFormAppService: React.FC = () => {
  const { data: session } = useSession();
  const user_az_id = session?.user?.sub;
  const user_email = session?.user?.email;

  //Get Access Token
  const token = session?.accessToken;

  const [resourceGroupName, setResourceGroupName] = useState(
    generateRandomName(24)
  ); // Random default value
  const [accountName, setAccountName] = useState(generateRandomName(24)); // Random default value
  const [location, setLocation] = useState("westeurope"); // Default to "westeurope"
  const [tags, setTags] = useState<{ [key: string]: string }>({
    env: "production",
    user: user_az_id || "",
  }); // Default value with example tag
  const [containerName, setContainerName] = useState("mycontainer"); // Default value
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState("");
  const [storageAccountName, setStorageAccountName] = useState(""); // State to store storage account name
  const [accessKey, setAccessKey] = useState(""); // State to store access key
  const [containers, setContainers] = useState<string[]>([]); // State to store fetched containers
  const [azureops, setAzureOps] = useState(""); // State for Azure Operations
  const [hasStorageAccount, setHasStorageAccount] = useState(false); // State to check if user already has a storage account

  // Function to check if user already has a storage account
  const checkIfUserHasStorageAccount = async () => {
    if (!user_az_id) return; // No user ID available

    try {
      const response = await fetch(
        "/api/azure/storageaccount/check-storage-account",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ userId: user_az_id }),
        }
      );

      if (response.ok) {
        const data: {
          hasStorageAccount: boolean;
          storageAccountName: string;
          accessKey: string;
          containerName: string;
          resourceGroupName: string;
        } = await response.json();
        // setHasStorageAccount(data.hasStorageAccount);
        setHasStorageAccount(data.storageAccountName !== "");
        setStorageAccountName(data.storageAccountName);
        setAccessKey(data.accessKey);
        setContainerName(data.containerName || "mycontainer");
        setResourceGroupName(data.resourceGroupName);
        // console.log("resourceGroupName:", data.resourceGroupName);
      } else {
        console.error("Error checking storage account existence.");
      }
    } catch (error) {
      console.error("Error:", error);
    }
  };

  // Funktion zum Abrufen des Werts aus der Datenbank
  const fetchData = async () => {
    try {
      const operation = azureops;
      const value = "status";

      const responseOps = await fetch("/api/azure/resources/manageoperations", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          resourceGroupName,
          operation,
          value,
        }),
      });

      const data = await responseOps.json();

      if (azureops === "create") {
        console.log("Data: ", data.data.creation);
        if (data.data.creation === "Success") {
          setMessage("Storage account created successfully!");
          setHasStorageAccount(true); // Update the state to reflect the creation
          setLoading(false);
        }
      }
      if (azureops === "delete") {
        console.log("Data: ", data.data.deletion);
        if (data.data.deletion === "Success") {
          setMessage("Storage account successfully deleted!");
          setHasStorageAccount(false); // Update the state to reflect the creation
          setLoading(false);
        }
      }
    } catch (error) {
      console.error("Fehler beim Abrufen der Daten:", error);
    }
  };

  useEffect(() => {
    checkIfUserHasStorageAccount();
  }, [session]);

  // Überwacht Datenbankwertänderungen
  useEffect(() => {
    // Datenbankabfrage alle 5 Sekunden
    if (loading === false) return; // Nur abfragen, wenn der Ladezustand true ist
    const intervalId = setInterval(fetchData, 5000);

    return () => clearInterval(intervalId); // Bereinigen, wenn die Komponente unmontiert wird
  }, [loading === true]);

  // Delete storage account

  const handleDeleteStorageAccount = async () => {
    if (!accountName) return; // No storage account name available

    // Check if user has a storage account and get Resource Group Name
    checkIfUserHasStorageAccount();

    // console.log("Deleting resourceGroup:", resourceGroupName);

    setLoading(true);
    setMessage("");
    setAzureOps("delete");

    try {
      // Creating Records in Operationstable
      const operation = "delete";
      const value = "pending";

      // Call the function to create a record in the operation table
      const responseOps = await fetch("/api/azure/resources/manageoperations", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          resourceGroupName,
          operation,
          value,
        }),
      });

      const response = await fetch(
        "/api/azure/resources/deleteresourcegroupappservice",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: "Bearer " + token,
          },
          body: JSON.stringify({
            tagKey: "user",
            tagValue: user_az_id,
            user_email,
            resourceGroupName: resourceGroupName,
          }),
        }
      );

      if (response.ok) {
        const successData = await response.text();
        setMessage(`Success: ${successData}`);
      } else {
        const errorData = await response.text();
        setMessage(`Error: ${errorData}`);
      }
    } catch (error) {
      setMessage(`Error: ${error}`);
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setMessage("");
    setAzureOps("create");
    setContainers([]); // Clear previous container list

    try {
      let randomName = resourceGroupName; // Start with the current state value

      // Check if Resource Group Name is empty
      if (resourceGroupName === "") {
        // console.error("Resource Group Name is empty");
        randomName = generateRandomName(24); // Generate a new name
        setResourceGroupName(randomName); // Update the state, but it may not reflect immediately
      }

      const newTags = { evn: "production", user: user_az_id };

      // Creating Records in Operationstable
      const operation = "create";
      const value = "pending";

      // Call the function to create a record in the operation table
      const responseOps = await fetch("/api/azure/resources/manageoperations", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          resourceGroupName: randomName, // Use the `randomName` directly
          tags: newTags,
          operation,
          value,
        }),
      });

      const response = await fetch(
        "/api/azure/resources/createstorageaccountappservice",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: "Bearer " + token,
          },
          body: JSON.stringify({
            resourceGroupName: randomName,
            accountName,
            location,
            tags: newTags,
            containerName,
            user_az_id,
            user_email,
          }),
        }
      );

      if (response.ok) {
        const successData = await response.text();
        setMessage(`Success: ${successData}`);
        checkIfUserHasStorageAccount();
      } else {
        const errorData = await response.text();
        setMessage(`Error: ${errorData}`);
      }
    } catch (error) {
      setMessage(`Error: ${error}`);
    }
  };

  return (
    <div className="max-w-lg p-6 mx-auto mt-6 bg-gray-400 rounded-lg shadow-md">
      <h2 className="mb-4 text-2xl font-bold text-center">
        {hasStorageAccount
          ? "You already have a storage account"
          : "Create Storage Account"}
      </h2>
      {/* Delete Storage Account Button */}
      {hasStorageAccount && (
        <>
          <LoadingButton
            loading={loading}
            onClick={handleDeleteStorageAccount}
            startlabel="Deleting Storage Account"
            loadinglabel="Deleting ..."
          />
          <div className="flex justify-center items-center min-h-screen">
            <FileUploader
              storageAccountName={storageAccountName}
              accessKey={accessKey}
              containerName={containerName}
            />
          </div>
        </>
      )}
      {!hasStorageAccount && (
        <LoadingButton
          loading={loading}
          onClick={handleSubmit}
          startlabel="Creating Storage Account"
          loadinglabel="Creating ..."
        />
      )}

      {/* Message display */}
      {message && (
        <p className="mt-4 text-sm font-medium text-center">{message}</p>
      )}
      {/* Containers list display */}
      {containers.length > 0 && (
        <div className="mt-6">
          <h3 className="mb-2 text-lg font-semibold">Containers:</h3>
          <ul className="list-disc list-inside">
            {containers.map((container) => (
              <li className="text-gray-600" key={container}>
                {container}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
};

export default CreateStorageAccountFormAppService;

Let’s bring it all together

Aim of the API Routes

  1. Create Storage Account (createstorageaccountappservice): Creates a new Azure Storage Account, verifying admin privileges before proceeding.

  2. Delete Resource Group (deleteresourcegroupappservice): Deletes Azure Resource Groups by tag, allowing for selective deletion of resources.

  3. File Delete (filedelete): Deletes a specific file in a storage container based on filename.

  4. Delete All Files (deleteall): Deletes all files in a given Azure Blob Storage container.

  5. SAS Token (sastoken): Generates a SAS token that enables secure file uploads to Azure Blob Storage.

  6. List Files (list): Retrieves and lists all files in a storage container.

Aim of the Components

  1. CreateStorageAccountFormAppService : Facilitates the creation, deletion, and management of Azure Storage Accounts. It manages user inputs and validates storage account existence. If an account exists, it integrates the FileUploader for file operations.

  2. FileUploader: Handles uploading, listing, and deleting files in Azure Blob Storage. It provides a drag-and-drop interface, monitors progress, and uses Azure SAS tokens for secure uploads.

Conclusion

In this post, we’ve built a robust file management system for a SaaS solution using Next.js 14 and Azure. By integrating a set of API routes, we allow users to create and delete Azure Storage Accounts, upload files using SAS tokens, and manage the files through a user-friendly interface.

The combination of components like CreateStorageAccountFormAppService and FileUploader offers a seamless experience for managing cloud resources and files. This setup provides a scalable and secure architecture for handling storage in SaaS applications.

Cloudapp-dev, and before you leave us

Thank you for reading until the end. Before you go:

Please consider clapping and following the writer! 👏 on our Medium Account

Or follow us on twitter -> Cloudapp.dev

Related articles