In my first story (Manipulate Azure AD B2C Data), I explained the basic setup of Prisma for PostgreSQL (I used the free plan from Neon.tech) and how you can update your User Objects within the Azure AD Object Store. We will examine how you can further manipulate your data in both sources.
Here is the GitHub repo with the full code.
In my previous story, regarding CRUD operations on Azure AD B2C, I explained how you can “Patch” (modify) data attributes of your User Objects and how you can create a new user record with the famous ORM Prisma in your PostgreSQL DB (I used the free offering of neon.tech).
Below is the Next.js code snippet for storing a new user in the “roles” table with Prisma. We pass only one attribute, “name”.
data = await prisma.roles.create({
data: {
name: name,
},
});Prisma Schema
Here is the model definition for the roles tables in my “schema.prisma” file
model roles {
id Int @id @default(autoincrement())
name String
addedOn DateTime @default(now())
}And here is the complete schema.prisma file, with all the tables that we defined in the last stories
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
model logins {
id Int @id @default(autoincrement())
email String
name String
logins Int
objectId String?
lastLogin DateTime @default(now())
}
model ApiToken {
tokenId String @id @default(cuid())
accessToken String @db.VarChar
tokenType String @default("Azure")
expirationDate DateTime
addedOn DateTime @default(now())
}
model roles {
id Int @id @default(autoincrement())
name String
addedOn DateTime @default(now())
}Prisma Delete Record
To delete this new record, I use this code snippet.
data = await prisma.roles.delete({
where: {
id: id,
},
});Here is the complete code from the component “promoteUser.component.tsx”, where I show the Users from Azure AD B2C directory and on each line a “Select” for the roles, which we defined via Prisma in our “roles” table and one button for the role change and a new button for “User Delete”. I am using the Table component from Tremor/React as a base structure and three different API calls for CRUD operations
# Deleting User on Azure AD B2C
/api/azure/user/delete
and
# Patch / Modify User on Azure AD B2C
/api/user/promote
and
# Get Azure AD B2C Users
/api/azure/user"use client";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { useState, useEffect } from "react";
import type { LocaleTypes } from "@/app/i18n/settings";
import { useTranslation } from "@/app/i18n/client";
import { useParams } from "next/navigation";
import {
Text,
Title,
Button,
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
} from "@tremor/react";
interface User {
id: string;
name: string;
role: string;
email: string;
created: string;
}
interface Role {
id: number;
name: string;
addedOn: Date;
}
let b2cextensionuservalue: string | unknown = "";
const PromoteRole = ({
roles,
b2cextensionuser,
tld,
}: {
roles: Role[];
b2cextensionuser: string;
tld: string;
}) => {
const [usercomplete, setUsercomplete] = useState<User[]>([]);
const [time, setTime] = useState(Date.now());
const [value, setValue] = useState("");
const locale = useParams()?.locale as LocaleTypes;
const { t } = useTranslation(locale, "common");
const ChangeHandler = (e: any) => {
let role = e.target.value;
// console.log("role:", role);
setValue(role);
};
const ondelete = async (id: string) => {
// fields check
let response = await fetch("/api/azure/user/delete?userid=" + id);
// // get the data
let data = response.status;
console.log("data:", data);
if (data === 204) {
// reset the fields
toast.success("Deleted", {
position: "top-right",
autoClose: 1000,
});
//Set Time to trigger UseEffect
setTime(Date.now());
} else {
toast.error(
"Oups, etwas ist schief gelaufen, bitte probieren Sie es noch einmal oder kontaktieren Sie uns.",
{
position: "top-right",
autoClose: 1000,
}
);
}
};
const onpromote = async (id: string, role: string) => {
// user structure
let user = {
id,
role,
};
if (!role) return toast.error(t("user.rolemissing"), { autoClose: 1000 });
let response = await fetch("/api/user/promote", {
method: "POST",
body: JSON.stringify(user),
});
let data = response.status;
if (data === 204) {
toast.success(t("user.changedto") + " " + role, {
position: "top-right",
autoClose: 1000,
});
// reset the fields
setValue("");
setTime(Date.now());
} else {
toast.error(
"Oups, etwas ist schief gelaufen, bitte probieren Sie es noch einmal oder kontaktieren Sie uns.",
{
position: "top-right",
autoClose: 1000,
}
);
}
};
useEffect(() => {
const fetchUsers = async () => {
const response = await fetch("/api/azure/user", {
method: "GET",
});
// get the data
let data = await response.json();
const users = data.data.value;
const usercompletenew: any[] = [];
users.forEach((user: any) => {
b2cextensionuservalue = "";
Object.entries(user).forEach((entry) => {
const [key, value] = entry;
if (key === b2cextensionuser) {
b2cextensionuservalue = value || "";
}
});
usercompletenew.push({
id: user.id,
name: user.displayName,
role: b2cextensionuservalue,
email:
user.identities[0].signInType === "emailAddress"
? user.identities[0].issuerAssignedId
: user.otherMails[0],
created: user.createdDateTime,
});
});
setUsercomplete(usercompletenew);
};
fetchUsers();
}, [time, b2cextensionuser]);
return (
<>
<ToastContainer />
<Title>{t("search.dashboardsearchhighline")}</Title>
<Text>{t("search.dashboardsearchdescription")}</Text>
<Table className="mt-4 border border-gray-700 rounded-md">
<TableHead>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>{t("user.role")}</TableHeaderCell>
<TableHeaderCell>Email</TableHeaderCell>
<TableHeaderCell>{t("user.createdat")}</TableHeaderCell>
<TableHeaderCell>Role Selection</TableHeaderCell>
<TableHeaderCell>{t("user.actions")}</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{usercomplete.map((datauseritem: User, index: number) => (
<TableRow key={index}>
<TableCell>
<Text>{datauseritem.name}</Text>
</TableCell>
<TableCell>
<Text>{datauseritem.role}</Text>
</TableCell>
<TableCell>
<Text>{datauseritem.email}</Text>
</TableCell>
<TableCell>
<Text>{datauseritem.created}</Text>
</TableCell>
<TableCell>
<select
id={index.toString()}
className="bg-gray-500 border border-gray-500 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 hover:bg-gray-600 hover:border-gray-700"
onChange={(e) => ChangeHandler(e)}
>
<option value="">{t("user.selectrole")}</option>
{roles.map((role: Role, index: number) => (
<option key={index} value={role.name || ""}>
{role.name}
</option>
))}
</select>
</TableCell>
<TableCell>
<Button
size="sm"
variant="primary"
color="gray"
onClick={() => onpromote(datauseritem.id, value)}
>
{t("user.promoteadmin")}
</Button>
</TableCell>
{/* Delete user */}
<TableCell>
<Button
size="sm"
variant="primary"
color="gray"
onClick={() => ondelete(datauseritem.id)}
>
{t("user.deleteuser")}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
};
export default PromoteRole;API for Azure AD B2C User Deletion
If we have a valid Access Token in our ApiToken table, we use that one. Otherwise, we generate and save a new one. We can perform the delete call with the Token and the Azure AD B2C ID of the user object.
import type { AuthTokenResp } from "@/types/api";
import prisma from "@/lib/prisma";
import { unixTimestampToDate, urlBase64Decode } from "@/lib/utils";
import { NextResponse, NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";
async function AzureToken() {
const formdata = new FormData();
formdata.append("grant_type", "client_credentials");
formdata.append("client_id", process.env.AZURE_CLIENT_ID || "");
formdata.append("client_secret", process.env.AZURE_CLIENT_SECRET || "");
formdata.append("scope", process.env.AZURE_SCOPE || "");
if (!process.env.AZURE_TOKEN_URL) {
throw new Error("AZURE_TOKEN_URL is not set");
}
const res = await fetch(process.env.AZURE_TOKEN_URL, {
method: "POST",
body: formdata,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`
Failed to fetch data
Status: ${res.status}
Response: ${text}
`);
}
const newTokens: AuthTokenResp = await res.json();
return newTokens;
}
async function AzureData() {
const mostRecentApiToken = await prisma.apiToken.findFirst({
where: { tokenType: "Azure" },
orderBy: {
addedOn: "desc",
},
});
let token: string = "";
if (mostRecentApiToken) {
const { accessToken, expirationDate } = mostRecentApiToken;
const tokenExpirationDate = new Date(expirationDate);
const currentDate = new Date();
token = accessToken;
// compare the current date to the expiration date
if (currentDate > tokenExpirationDate) {
//Get Access Token
const tokenazure: any = await AzureToken();
const newTokens = tokenazure;
const accessToken = newTokens.access_token;
const { exp, nbf } = urlBase64Decode(accessToken.split(".")[1]);
const newExpirationDate = unixTimestampToDate(exp);
await prisma.apiToken.create({
data: {
accessToken: accessToken,
expirationDate: newExpirationDate,
tokenType: "Azure",
addedOn: new Date(),
},
});
token = accessToken;
}
} else {
//Get Access Token
const tokenazure: any = await AzureToken();
const newTokens = tokenazure;
const accessToken = newTokens.access_token;
const { exp, nbf } = urlBase64Decode(accessToken.split(".")[1]);
const newExpirationDate = unixTimestampToDate(exp);
await prisma.apiToken.create({
data: {
accessToken: accessToken,
expirationDate: newExpirationDate,
tokenType: "Azure",
addedOn: new Date(),
},
});
token = accessToken;
}
return token;
}
export async function GET(req: NextRequest) {
const authtoken = await getToken({ req });
const isAdmin = authtoken?.role === "Admin";
if (!authtoken || !isAdmin) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let data = null;
const url = new URL(req.url);
const userId = url.searchParams.get("userid");
//Get Access Token
const token = await AzureData();
const GraphUrl = `https://graph.microsoft.com/v1.0/users/${userId}`;
const res = await fetch(GraphUrl, {
method: "DELETE",
headers: {
Authorization: "Bearer " + token,
},
});
return new NextResponse(undefined, {
status: res.status,
statusText: res.statusText,
});
}Needed Azure AD B2C permissions for User deletion
We are using the service principal (app registration) “TestProd”, which I showed in this story.
However, the permission for user deletion is not given, so we have to adapt them.
To fix the issue, you need to assign your service principal to a directory role e.g. User administrator/ Global administrator.
Navigate to the Azure Active Directory in the Azure portal -> Roles and administrators -> click User administrator or Global administrator -> Add assignment -> search by your service principal name(must search) -> find it and select it -> click Select.
API for removing Role Records with Prisma
We pass the UserID from our Component, which we then use to delete the corresponding record from the Roles Table with Prisma.
import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
export async function POST(req: Request) {
// Extract the `messages` from the body of the request
const { id } = await req.json();
let data: any = null;
if (id) {
data = await prisma.roles.delete({
where: {
id: id,
},
});
}
if (data) {
data["status"] = "success";
}
return NextResponse.json({
data,
});
}Component for handling the roles in our Dashboard section
Below, you will find the component that we use to list the created roles. The component displays a “Delete” button on every row so that you can easily delete it.
"use client";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
Badge,
Button,
Text,
} from "@tremor/react";
import type { LocaleTypes } from "@/app/i18n/settings";
import { useTranslation } from "@/app/i18n/client";
import { useParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
interface Role {
id: number;
name: string;
addedOn: Date;
}
export default function RolesTable({ roles }: { roles: Role[] }) {
const router = useRouter();
let intlDateObj = new Intl.DateTimeFormat("de-DE", {
dateStyle: "full",
timeStyle: "long",
timeZone: "Europe/Paris",
});
const locale = useParams()?.locale as LocaleTypes;
const { t } = useTranslation(locale, "common");
const ondelete = async (id: number) => {
// user structure
let user = {
id,
};
let response = await fetch("/api/user/deleterole", {
method: "POST",
body: JSON.stringify(user),
});
//get the data
let data = await response.json();
if (data.data.status === "success") {
toast.success("Deleted", {
position: "top-right",
autoClose: 1000,
});
// Refresh page after User deletion
router.refresh();
} else {
toast.error(
"Oups, etwas ist schief gelaufen, bitte probieren Sie es noch einmal oder kontaktieren Sie uns.",
{
position: "top-right",
autoClose: 1000,
}
);
}
};
return (
<>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>{t("user.roles")}</TableHeaderCell>
<TableHeaderCell>{t("user.createdat")}</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{roles.map((role: Role) => (
<TableRow key={role.id}>
<TableCell>
<Text>{role.name}</Text>
</TableCell>
<TableCell>
<Text>{intlDateObj.format(role.addedOn)}</Text>
</TableCell>
<TableCell>
<Button
size="xs"
variant="primary"
color="gray"
onClick={() => ondelete(role.id)}
>
Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
}Cloudapp-dev, and before you leave us
Thank you for reading until the end. Before you go:



