Loading...
Azure-Ad-B2C-Rest-Part2
Author Cloudapp
E.G.

Azure AD B2C/Prisma ORM - Datenkontrolle über REST - Teil 2

4. Juni 2024
Inhaltsverzeichnis

In meiner ersten Geschichte (So manipulieren Sie Ihre Azure AD B2C-Daten über Ihre REST-API) habe ich die grundlegende Einrichtung von Prisma für PostgreSQL (ich habe den kostenlosen Plan von Neon.tech verwendet) und wie man Benutzerobjekte im Azure AD-Objektspeicher aktualisieren kann, erklärt. Wir werden untersuchen, wie Sie Ihre Daten in beiden Quellen weiter manipulieren können.

Hier ist das GitHub repo mit dem vollständigen Code.

In meiner vorherigen Story über CRUD-Operationen in Azure AD B2C habe ich erklärt, wie Sie Datenattribute Ihrer Benutzerobjekte „patchen“ (ändern) können und wie Sie einen neuen Benutzerdatensatz mit dem bekannten ORM Prisma in Ihrer PostgreSQL-Datenbank erstellen können (ich habe das kostenlose Angebot von neon.tech genutzt).

Unten ist der Next.js-Codeausschnitt zum Speichern eines neuen Benutzers in der „roles“-Tabelle mit Prisma. Wir übergeben nur ein Attribut, „name“.

data = await prisma.roles.create({
      data: {
        name: name,
      },
    });

Prisma Schema

Hier ist die Modelldefinition für die Rollen-Tabellen in meiner „schema.prisma“-Datei.

model roles {
  id      Int      @id @default(autoincrement())
  name    String
  addedOn DateTime @default(now())
}

Und hier ist die vollständige schema.prisma-Datei mit allen Tabellen, die wir in den letzten Stories definiert haben. Hier ein Link zur Übersicht aller Prisma-Anleitungen

// 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 Datensatz löschen

Wir benutzen dieses Code Snippet für das Löschen des Datensatzes

data = await prisma.roles.delete({
      where: {
        id: id,
      },
    });

Hier ist der vollständige Code der Komponente „promoteUser.component.tsx“, in dem ich die Benutzer aus dem Azure AD B2C-Verzeichnis anzeige. In jeder Zeile gibt es ein „Select“ für die Rollen, die wir über Prisma in unserer „roles“-Tabelle definiert haben, sowie einen Button für die Rollenänderung und einen neuen Button für „Benutzer löschen“. Ich verwende die Table-Komponente von Tremor/React als Basisstruktur und drei verschiedene API-Aufrufe für CRUD-Operationen.

# 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 für die Löschung von Benutzern in Azure AD B2C

Wenn wir ein gültiges Zugriffstoken in unserer ApiToken-Tabelle haben, verwenden wir dieses. Andernfalls generieren und speichern wir ein neues. Mit dem Token und der Azure AD B2C ID des Benutzerobjekts können wir den Löschaufruf durchführen.

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,
  });
}

Benötigte Azure AD B2C-Berechtigungen für die Löschung von Benutzern

Wir verwenden das Dienstprinzipal (App-Registrierung) "TestProd", das ich in dieser Story gezeigt habe. Allerdings ist die Berechtigung zur Löschung von Benutzern nicht gegeben, daher müssen wir diese anpassen.

Um das Problem zu beheben, müssen Sie Ihrem Dienstprinzipal eine Verzeichnisrolle zuweisen, z.B. Benutzeradministrator oder globaler Administrator. Navigieren Sie im Azure-Portal zu Azure Active Directory -> Rollen und Administratoren -> klicken Sie auf Benutzeradministrator oder globaler Administrator -> Zuweisung hinzufügen -> suchen Sie nach Ihrem Dienstprinzipalnamen (muss gesucht werden) -> finden und auswählen -> auf Auswählen klicken.

API für das Löschen der "Rollen" Datensätze mit Prisma

Wir übergeben die UserID aus unserer Komponente, die wir dann verwenden, um den entsprechenden Datensatz aus der Roles-Tabelle mit Prisma zu löschen.

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,
  });
}

Komponente für die Verwaltung der "Rollen" im Dashboardbereich

Unten finden Sie die Komponente, die wir verwenden, um die erstellten Rollen aufzulisten. Die Komponente zeigt in jeder Zeile eine „Löschen“-Schaltfläche an, damit Sie sie einfach löschen können.

"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 und bevor Sie uns verlassen

Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:

Wenn Ihnen gefallen hat was Sie gelesen haben oder wenn es Ihnen sogar geholfen hat, dann würden wir uns über einen "Clap" 👏 oder einen neuen Follower auf unseren Medium Account sehr freuen.

Verwandte Artikel