Table of Contents
- Github Repo and Example Page with Final Result
- Setting Up Storage Accounts via API
- Deleting Azure Resource Groups via API with Admin Authorization
- How It Works:
- Uploading Files with the Frontend Component
- Deleting Files and Storage Management
- Uploading Files via SAS Token
- File Deletion with Search
- Main Component (createstorageaccountform_appservice.component.tsx)
- Let’s bring it all together
- Aim of the API Routes
- Aim of the Components
- Conclusion
- Cloudapp-dev, and before you leave us
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/createstorageaccountappserviceHere’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:
Authorization:
It validates if the user has an Admin role using the JWT token from next-auth/jwt.
Payload Construction:
The API builds a payload that contains the necessary data like tagKey, tagValue, resourceGroupName, and user_email.
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.
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).
File Deletion with Search
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
Create Storage Account (createstorageaccountappservice): Creates a new Azure Storage Account, verifying admin privileges before proceeding.
Delete Resource Group (deleteresourcegroupappservice): Deletes Azure Resource Groups by tag, allowing for selective deletion of resources.
File Delete (filedelete): Deletes a specific file in a storage container based on filename.
Delete All Files (deleteall): Deletes all files in a given Azure Blob Storage container.
SAS Token (sastoken): Generates a SAS token that enables secure file uploads to Azure Blob Storage.
List Files (list): Retrieves and lists all files in a storage container.
Aim of the Components
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.
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:




