Inhaltsverzeichnis
Entdecken Sie die Dokumente »
Fehler melden
·
Anforderungsfunktion
·
Treten Sie unserem Discord bei
·
Roadmap
·
X
·
Benachrichtigungsverzeichnis
Novu bietet eine einheitliche API, die das Senden von Benachrichtigungen über mehrere Kanäle, einschließlich In-App, Push, E-Mail, SMS und Chat, vereinfacht. Mit Novu können Sie benutzerdefinierte Workflows erstellen und Bedingungen für jeden Kanal definieren, um sicherzustellen, dass Ihre Benachrichtigungen so effektiv wie möglich zugestellt werden.
Novu implementierte Clerk als Benutzerverwaltungslösung (Authentifizierungsinfrastruktur), die den Grundstein für die Bereitstellung von SAML Single Sign-On (SSO)-Funktionalität, Google und GitHub als OAuth-Anbieter, Multi-Faktor-Authentifizierung und rollenbasierter Kontosteuerung (RBAC) legte ) und mehr.
Ein Entwickler namens Adam hat es mit tatkräftiger Unterstützung des Plattformingenieurs Denis implementiert.
Wie die meisten Projekte begann auch dieses mit dem Rückstand. Nicht bevor Dutzende von Kunden danach gefragt und die Anfrage in unserer Roadmap stark positiv bewertet haben.
Als Benachrichtigungsinfrastrukturlösung umfasst ein Teil unserer App und Architektur die Verwaltung von Benutzern anhand der Registrierungen, Anmeldungen und Sitzungsprotokolle, damit Benutzer Teammitglieder in die Organisation einladen und den Zugriff jeder Rolle verwalten können.
Es geht um Prioritäten. Das Hauptaugenmerk von Novu liegt darauf, alle Probleme im Zusammenhang mit Benachrichtigungen und Nachrichtenverwaltung zu lösen, damit unsere Benutzer dies nicht tun müssen. Deshalb verbringen wir unsere Zeit damit, die bestmögliche Erfahrung beim Erstellen und Verwalten von Benachrichtigungsworkflows zu bieten, Entwicklern die Möglichkeit zu geben, ihre Zeit zu sparen, und die Zusammenarbeit zwischen Produkt- und Marketingteams zu optimieren.
Eine effektive Benutzerverwaltung fällt nicht unter den „Kernwert“, dem wir uns verschrieben haben.
So wie wir von Ihnen erwarten, dass Sie die Last der technischen Benachrichtigungen auf unser Fachwissen übertragen, haben wir die Last der technischen effektiven Benutzerverwaltung auf die Fachkompetenz von Clerk verlagert.
Unnötig zu erwähnen, dass unser Team vom ersten Tag an intern eine großartige Authentifizierungs- und Autorisierungsinfrastruktur aufgebaut hat, die auf einer maßgeschneiderten und gut gestalteten Architektur basiert.
Mit zunehmendem Level konzentrieren wir uns noch mehr auf die Perfektionierung des Benachrichtigungsentwicklungserlebnisses.
Wir erwarten von Entwicklern und Ingenieuren, dass sie das Rad nicht neu erfinden und Novu die Verwaltung von Benachrichtigungen überlassen, genauso wie wir uns dafür entscheiden, bewährte, getestete und führende Lösungen für andere Aspekte unseres Produkts zu nutzen: MongoDB für die Datenbank, Stripe für Zahlungen und jetzt Sachbearbeiter für Benutzerverwaltung. Wir lassen den Worten Taten folgen.
Schaffen Sie ein sicheres und benutzerfreundliches Erlebnis für unsere Benutzer.
Wenn man den ersten Entwurf für dieses Projekt skizziert, wirkt er möglicherweise kurz und unkompliziert und erweckt möglicherweise sogar den Eindruck, dass er an einem Wochenende fertiggestellt werden könnte.
Die Checkliste für den ersten Entwurf:
Beachten Sie, dass das Projekt nicht genügend Feedback und Input erhalten hat, wenn sich der ursprüngliche Entwurf nicht geändert hat. Natürlich ist die Liste länger geworden.
Die aktuelle Checkliste:
Nachdem der Umfang des Projekts ermittelt wurde, besteht der nächste Schritt darin, Recherchen durchzuführen und die Ressourcen zu bewerten, die zum Erreichen des gewünschten Ergebnisses erforderlich sind.
Dieser Prozess umfasst:
Sie müssen den aktuellen Zustand und jede Schicht des Produkts genau kennen:
Und mehr.
Beschreiben Sie die Migrationsspezifikationen (was intern bleibt und verdrängt werden sollte)
Abwärtskompatibilität
Versuchen Sie, Hinweise auf ähnliche Projekte zu finden, vielleicht von ehemaligen Kollegen, und lernen Sie aus deren Prozess und Empfehlungen
Versuchen Sie, Open-Source-Lösungen zu finden
Finden Sie heraus, ob es Anbieter (Lösungen von Drittanbietern) gibt, und vergleichen Sie diese.
Und mehr.
In einem weiteren Blogbeitrag werden wir untersuchen, wie wir Lösungen (oder Produkte) von Drittanbietern als Service/Infrastruktur als Dienstleistungsunternehmen bewerten und vergleichen.
Unzureichende Recherche oder ungenaue Bewertung führen in der Regel zu technischen Schulden und zukünftigen Ressourcenverlusten, wie z. B. Engineering-Zeit beim Hinzufügen zusätzlicher Funktionen und Wartung, die eine Umgestaltung der gesamten Sache erfordern. Suchen Sie also nach den versteckten Kosten jeder Option.
Erfahrene Teamleiter wissen, wie sie den Return on Investment (ROI) jeder Option bewerten, was ihnen hilft, die beste Entscheidung für das Unternehmen zu treffen.
Genau so sind wir bei Clerk gelandet. Ihre Lösung deckt die meisten unserer Anwendungsfälle ab und aus geschäftlicher Sicht ist der ROI bei der Implementierung zur Verwaltung der Benutzer- und Organisationsebene sinnvoll.
Der Novu-Dienst enthält viele Mikrodienste und Aspekte wie:
Das Diagramm unten zeigt eine vereinfachte Version der API-Struktur von Novu, die sich nur auf die Authentifizierung und Autorisierung von Novu-Benutzern und -Organisationen vor der Implementierung von Clerk konzentriert.
Wir verwenden MongoDB, um alle Daten zu speichern, die Novu benötigt, jeden Benutzer, jede Organisation, jeden Mieter, jeden Abonnenten, jedes Thema … kurz gesagt, alles.
Da Clerk über eine eigene Datenbank zur Benutzerverwaltung verfügt, mussten wir die Migration und Synchronisierung zwischen den Datenbanken sehr sorgfältig und präzise durchführen.
Eines der wichtigsten Dinge, die wir sicherstellen mussten, ist, dass sich das UserSessionData-Objekt nicht ändert, um die Sitzung des Benutzers bei der Verwendung von Novu nicht zu unterbrechen. Es sollte kompatibel bleiben.
Hier können Sie das Beispiel der jwt.stratgy.ts-Datei sehen:
//jwt.stratgy.ts import type http from 'http'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ApiAuthSchemeEnum, HttpRequestHeaderKeysEnum, UserSessionData } from '@novu/shared'; import { AuthService, Instrument } from '@novu/application-generic'; import { EnvironmentRepository } from '@novu/dal'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private readonly authService: AuthService, private environmentRepository: EnvironmentRepository) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: process.env.JWT_SECRET, passReqToCallback: true, }); } @Instrument() async validate(req: http.IncomingMessage, session: UserSessionData) { // Set the scheme to Bearer, meaning the user is authenticated via a JWT coming from Dashboard session.scheme = ApiAuthSchemeEnum.BEARER; const user = await this.authService.validateUser(session); if (!user) { throw new UnauthorizedException(); } await this.resolveEnvironmentId(req, session); return session; } @Instrument() async resolveEnvironmentId(req: http.IncomingMessage, session: UserSessionData) { // Fetch the environmentId from the request header const environmentIdFromHeader = (req.headers[HttpRequestHeaderKeysEnum.NOVU_ENVIRONMENT_ID.toLowerCase()] as string) || ''; /* * Ensure backwards compatibility with existing JWTs that contain environmentId * or cached SPA versions of Dashboard as there is no guarantee all current users * will have environmentId in localStorage instantly after the deployment. */ const environmentIdFromLegacyAuthToken = session.environmentId; let currentEnvironmentId = ''; if (environmentIdFromLegacyAuthToken) { currentEnvironmentId = environmentIdFromLegacyAuthToken; } else { const environments = await this.environmentRepository.findOrganizationEnvironments(session.organizationId); const environmentIds = environments.map((env) => env._id); const developmentEnvironmentId = environments.find((env) => env.name === 'Development')?._id || ''; currentEnvironmentId = developmentEnvironmentId; if (environmentIds.includes(environmentIdFromHeader)) { currentEnvironmentId = environmentIdFromHeader; } } session.environmentId = currentEnvironmentId; } }
Um die Kompatibilität mit dem Rest der App aufrechtzuerhalten, mussten wir die JWT-Nutzlast von Clerk in das zuvor vorhandene JWT-Format umwandeln.
So haben wir es gemacht:
async validate(payload: ClerkJwtPayload): Promise<IJwtClaims> { const jwtClaims: IJwtClaims = { // first time its clerk_id, after sync its novu internal id _id: payload.externalId || payload._id, firstName: payload.firstName, lastName: payload.lastName, email: payload.email, profilePicture: payload.profilePicture, // first time its clerk id, after sync its novu internal id organizationId: payload.externalOrgId || payload.org_id, environmentId: payload.environmentId, roles: payload.org_role ? [payload.org_role.replace('org:', '')] : [], exp: payload.exp, }; return jwtClaims; }
Hier können Sie das Dateibeispiel clerk.strategy.ts sehen:
import type http from 'http'; import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { passportJwtSecret } from 'jwks-rsa'; import { ApiAuthSchemeEnum, ClerkJwtPayload, HttpRequestHeaderKeysEnum, PassportStrategyEnum, UserSessionData, } from '@novu/shared'; import { EnvironmentRepository, EnvironmentEntity } from '@novu/dal'; import { LinkEntitiesService } from '../services/link-entities.service'; @Injectable() export class ClerkStrategy extends PassportStrategy(Strategy, PassportStrategyEnum.JWT_CLERK) { constructor(private environmentRepository: EnvironmentRepository, private linkEntitiesService: LinkEntitiesService) { super({ // ...configuration details }); } async validate(req: http.IncomingMessage, payload: ClerkJwtPayload) { const { internalUserId, internalOrgId } = await this.linkEntitiesService.linkInternalExternalEntities(req, payload); const session: UserSessionData = { _id: internalUserId, firstName: payload.firstName, lastName: payload.lastName, email: payload.email, profilePicture: payload.profilePicture, organizationId: internalOrgId, roles: payload.org_role ? [payload.org_role.replace('org:', '')] : [], exp: payload.exp, iss: payload.iss, scheme: ApiAuthSchemeEnum.BEARER, environmentId: undefined, }; await this.resolveEnvironmentId(req, session); return session; } // Other functions... }
Während das Ziel darin besteht, idealerweise nur Clerk zum Erstellen und Abrufen von Benutzern, Organisationen usw. zu verwenden, ist dies leider nicht vollständig möglich, da einige Metadaten über Benutzer und Organisationen auf performante Weise gespeichert und abgefragt werden müssen.
Hier ist ein Beispiel für eine Methode im Organisations-Repository von Novu:
async findPartnerConfigurationDetails(organizationId: string, userId: string, configurationId: string) { const organizationIds = await this.getUsersMembersOrganizationIds(userId); return await this.find( { _id: { $in: organizationIds }, 'partnerConfigurations.configurationId': configurationId, }, { 'partnerConfigurations.$': 1 } ); }
Diese Methode verwendet verschiedene MongoDB-spezifische Konstrukte, um ein Dokument zu filtern. Dies kann mit Clerk nicht auf performante Weise reproduziert werden, da es sich nicht um eine Datenbank handelt, die für solche Abfragen gedacht ist.
Was wir tun können, ist, diese Metadaten über die Organisation in unserer MongoDB-Organisationssammlung zu speichern und die Sammlung mithilfe von externalId mit der Clerk-Datenbank zu verknüpfen/synchronisieren.
Jetzt können wir Clerk und MongoDB kombinieren, um bei Bedarf die Metadaten abzufragen.
async findPartnerConfigurationDetails( organizationId: string, userId: string, configurationId: string ): Promise<OrganizationEntity[]> { const clerkOrganizations = await this.getUsersMembersOrganizations(userId); return await this.communityOrganizationRepository.find( { _id: { $in: clerkOrganizations.map((org) => org.id) }, 'partnerConfigurations.configurationId': configurationId, }, { 'partnerConfigurations.$': 1 } ); } private async getUsersMembersOrganizations(userId: string): Promise<Organization[]> { const userOrgMemberships = await this.clerkClient.users.getOrganizationMembershipList({ userId, }); return userOrgMemberships.data.map((membership) => membership.organization); }
Durch den Aufruf von getUsersMembersOrganizations ruft findPartnerConfigurationDetails die erforderlichen Organisationsdaten ab, um eine gefilterte Suche im communityOrganizationRepository durchzuführen und sicherzustellen, dass nur relevante Konfigurationen zurückgegeben werden.
Wir müssen nur Benutzer und Organisationen zwischen Clerk und Novu synchronisieren, die Organisationsmitglieder müssen nicht synchronisiert werden.
Es gibt zwei Möglichkeiten, wie die Datenbank-IDs synchronisiert werden:
Here is the flow we had in mind:
Note
In the application, we always expect Novu’s internal id on input and we always return internal id on output - its important for the application to work as is without major changes to the rest of the code.
API expects internal _id everywhere and it needs to be MongoDB ObjectID type, because it parses this user id back to ObjectID e.g. when creating new environment or any other entity which needs reference to user.
The same logic applies to organizations; just the endpoint is different.
Users
For the users, we store everything in Clerk. All the properties are mostly just simple key/value pairs and we don’t need any advanced filtering on them, therefore they can be retrieved and updated directly in Clerk.
In internal MongoDB, we store just the user internal and external ids.
The original Novu user properties are stored in Clerk’s publicMetadata :
export type UserPublicMetadata = { profilePicture?: string | null; showOnBoardingTour?: number; };
There are also many other attributes coming from Clerk which can be set on the user.
Organizations
For the organizations, we store everything in Clerk except for apiServiceLevel, partnerConfigurations, and branding since they are “native” to Clerk and we update those attributes directly there via frontend components and so we don’t need to sync with our internal DB after we change organization name or logo via Clerk component.
The goal here was to replace the community (open source) implementation with Clerk while being minimally invasive to the application and to keep the Clerk implementation in a separate package.
This means we need to keep the changed providers (OrganizationRepository, AuthService…) on the same place with the same name so we don’t break the imports all over the place, but we need to change their body to be different based on feature flag.
The other option would be to change all of these providers in the 100+ of files and then import the EE(enterprise edition) package everywhere, which is probably not a good idea.
This turned out to be quite challenging due to the fact that users, organization and members are relatively deeply integrated to the application itself, referenced in a lot of places and they’re also tied to MongoDB specifics such as ObjectID or queries (create, update, findOne …).
The idea is to provide different implementation using NestJS dynamic custom providers where we are able to inject different class/service on compile time based on the enterprise feature flag.
This is the most promising solution we found while keeping the rest of the app mostly untouched, there are some drawbacks explained later.
We have two implementations of AuthService - community and enterprise one (in private package), we inject one of those as AUTH_SERVICE provider.
We need to however have a common interface for both IAuthService
Since we also need to change the AuthModule, we initialize two different modules based on the feature flag like this:
function getModuleConfig(): ModuleMetadata { if (process.env.NOVU_ENTERPRISE === 'true') { return getEEModuleConfig(); } else { return getCommunityAuthModuleConfig(); } } @Global() @Module(getModuleConfig()) export class AuthModule { public configure(consumer: MiddlewareConsumer) { if (process.env.NOVU_ENTERPRISE !== 'true') { configure(consumer); } } }
The reason why the EEModule can be a standalone module in the @novu/ee-auth package which we would just import instead of the original AuthModule and instead we are initializing one module conditionally inside API, is that we are reusing some original providers in the EE one - e.g. ApiKeyStrategy , RolesGuard, EnvironmentGuard etc which resides directly in API.
We would need to import them in the @novu/ee-auth package which would require to export these things somewhere (probably in some shared package) and it introduces other issues like circular deps etc - it can be however refactored later.
Same logic applies for the repositories. No module is being initialized here, they’re just directly injected to the original repository classes.
The controllers are being conditionally imported from inside @novu/api . The reason for that is the same as in the auth module, there are too many imports that the controllers uses, that we would either need to move to @novu/ee-auth or move them to a separate shared package - which would then trigger a much bigger change to the other unrelated parts of the app, which would increase the scope of this change.
function getControllers() { if (process.env.NOVU_ENTERPRISE === 'true') { return [EEOrganizationController]; } return [OrganizationController]; } @Module({ controllers: [...getControllers()], }) export class OrganizationModule implements NestModule { ... }
The main issue here is the need for common interface for both of the classes - community and enterprise. You want to remain compatible in both community and enterprise versions, so when there is a this.organizationService.getOrganizations() method being called in 50 places in the app - you need an enterprise equivalent with the same name otherwise you need to change 50 places to call something else.
This results in not-so-strict typing and methods without implementation
We need to have a common interface for both, however the community one relies on MongoDB methods and needs different method arguments as the enterprise one which causes a use of any to forcefully fit both classes etc.
In some cases we don’t need the method at all, so we need to throw Not Implemented .
We modified the endpoints as follows:
Lead Engineer: Adam Chmara
Platform Team Lead: Denis Kralj
Our implementation approach comes to the fact that we offloaded the Users, Organizations and Members management to Clerk.
The data property injection to Novu’s Controllers (endpoints) layer, Business layer and data layer happens based on “Enterprise” feature flag validation.
We are leveraging pre-built Clerk components on the frontend and reducing the need to build and maintain our own custom implementation on the backend.
You can also observe below the diagram of the current state after implementing Clerk.
Als wir uns entschieden haben, Clerk für die Benutzerverwaltung zu implementieren, haben wir uns auch für die langfristigen Vorteile der Erweiterung der Fähigkeiten und Funktionen entschieden, die Clerk in Zukunft unterstützen und anbieten wird.
Hier sind einige Beispiele dafür, was wir in naher Zukunft unterstützen könnten:
Novu dieses Maß an detaillierter Flexibilität zu bieten, wäre aufgrund der potenziellen Komplexität der Implementierung möglicherweise außerhalb des Rahmens oder gar nicht möglich gewesen.
Benutzeridentitätswechsel
Unsere Kundenerfolgs- oder Supportteams könnten dies nutzen, um Probleme zu beheben, Support bereitzustellen oder Funktionen aus der Perspektive des imitierten Benutzers zu testen, ohne dessen Passwort oder andere Authentifizierungsdetails kennen zu müssen.
Einfach ausgedrückt können wir das Erlebnis für Novu-Benutzer problemlos verbessern, da die Authentifizierungsinfrastruktur jetzt dafür ausgelegt ist.
Wenn Sie zusätzliche Funktionen im Zusammenhang mit AuthN (oder anderen) vorschlagen möchten, besuchen Sie unsere Roadmap, um Anfragen zu überprüfen und positiv zu bewerten, oder reichen Sie Ihre Idee ein.
Gefällt Ihnen, was Sie gelesen haben? Klicken Sie auf „Folgen“, um weitere Updates zu erhalten, und hinterlassen Sie unten einen Kommentar. Ich würde ❤️ dein ?
hören
Das obige ist der detaillierte Inhalt vonVerlagerung der Benutzerverwaltung von unternehmensintern auf ein Produkt: Warum wir es getan haben und was wir daraus gelernt haben. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!