>  기사  >  웹 프론트엔드  >  사용자 관리를 사내에서 제품으로 이동: 이를 수행한 이유와 배운 내용

사용자 관리를 사내에서 제품으로 이동: 이를 수행한 이유와 배운 내용

PHPz
PHPz원래의
2024-08-13 06:40:09375검색

목차

  • TL;DR
  • 우리의 주요 목표
  • 연구 및 평가
  • 구현
  • JWT 전략 vs. 사무원 전략
  • Clerk와 Novu 간 동기화
    • 사용자 및 조직 동기화
    • Clerk vs Novu에 저장되는 내용
  • Enterprise Edition 공급자 삽입
    • AuthService 및 AuthModule - 동적 삽입
    • 저장소 - 사용자, 조직, 구성원
    • 컨트롤러
    • 이 접근 방식의 문제
  • 엔드포인트 수정
  • 고려 및 피해야 할 핵심 사항
  • 팀 스포트라이트
  • 요약
  • Hindsight 보너스 포인트

이 게시물은 Novu
에서 제공되었습니다.

Moving User Management from In-House to a Product: Why We Did It and What We Learned 노부크 / 새로운

오픈 소스 알림 플랫폼. 내장형 알림 센터, 이메일, 푸시 및 Slack 통합.

Moving User Management from In-House to a Product: Why We Did It and What We Learned

Moving User Management from In-House to a Product: Why We Did It and What We Learned Moving User Management from In-House to a Product: Why We Did It and What We Learned Moving User Management from In-House to a Product: Why We Did It and What We Learned

개발자를 위한 오픈 소스 알림 인프라

단일 API로 다채널 알림을 관리하는 최고의 서비스


문서 살펴보기 »

버그 신고 · 기능 요청 · 우리의 불화에 동참하세요 · 로드맵 · 엑스 · 알림 디렉토리

⭐️ 왜 Novu인가요?

Novu는 인앱, 푸시, 이메일, SMS, 채팅 등 여러 채널을 통해 알림을 간편하게 보낼 수 있는 통합 API를 제공합니다. Novu를 사용하면 사용자 정의 워크플로우를 생성하고 각 채널에 대한 조건을 정의하여 알림이 가능한 가장 효과적인 방법으로 전달되도록 할 수 있습니다.

✨ 특징

  • ? 모든 메시징 제공업체(인앱, 이메일, SMS, 푸시, 채팅)를 위한 단일 API
  • ? CI에서 배포되는 완전 관리형 GitOps Flow
  • ? Zod 또는 JSON 스키마를 사용하여 워크플로 및 단계 검증을 정의하세요
  • ? React 이메일/Maizzle/MJML 통합
  • ? 고급 레이아웃 및 디자인 관리를 위한 CMS 탑재
  • ? 단일 대시보드에서 다중 채널 메시지 디버깅 및 분석
  • ? 내장형 알림 센터…
GitHub에서 보기

TL;DR

Novu는 SAML Single Sign-On(SSO) 기능을 제공하기 위한 기반을 마련한 사용자 관리 솔루션(인증 인프라)으로 Clerk를 구현했고, OAuth 공급자로 Google과 GitHub, 다단계 인증, 역할 기반 계정 제어(RBAC)를 구현했습니다. ) 등이 있습니다.

Adam이라는 개발자가 플랫폼 엔지니어 Denis의 확실한 지원을 받아 이를 구현했습니다.


Moving User Management from In-House to a Product: Why We Did It and What We Learned

대부분의 프로젝트와 마찬가지로 이 프로젝트도 백로그에서 시작되었습니다. 이전에는 수십 명의 고객이 이를 요청했고 우리 로드맵에서 해당 요청에 크게 찬성표를 보냈습니다.

알림 인프라 솔루션인 앱 및 아키텍처에는 사용자가 팀 구성원을 조직에 초대하고 각 역할의 액세스를 관리할 수 있도록 가입, 로그인, 세션 로그에서 사용자를 관리하는 작업이 포함됩니다.

모든 것은 우선순위에 관한 것입니다. Novu의 핵심 초점은 알림 및 메시지 관리와 관련된 모든 문제를 해결하여 사용자가 그럴 필요가 없도록 하는 것입니다. 따라서 우리는 알림 워크플로우 구축 및 관리를 위한 최고의 경험을 제공하고, 개발자가 자신의 시간을 보호할 수 있도록 지원하며, 제품 팀과 마케팅 팀 간의 협업을 원활하게 하는 데 사이클을 사용합니다.
효과적인 사용자 관리는 우리가 추구하는 '핵심 가치'에 해당되지 않습니다.

귀하가 엔지니어링 알림 부담을 우리 전문 지식에 전가하기를 기대하는 것과 마찬가지로, 우리는 효과적인 사용자 관리 엔지니어링 부담을 서기의 전문 지식에 넘겼습니다.

말할 필요도 없이 우리 팀은 맞춤화되고 잘 설계된 아키텍처를 기반으로 처음부터 사내에서 훌륭한 인증 및 권한 부여 인프라를 구축했습니다.

레벨이 높아질수록 알림 개발 경험을 완벽하게 만드는 데 더욱 집중합니다.

우리는 데이터베이스용 MongoDB, 결제용 Stripe 등 제품의 다른 측면에 대해 검증되고 테스트되었으며 선도적인 솔루션을 활용하기로 선택한 것처럼 개발자와 엔지니어가 바퀴를 재발명하지 않고 Novu가 알림을 처리하도록 해주기를 기대합니다. 사용자 관리 담당 서기. 우리는 이야기를 나눕니다.


우리의 주요 목표

사용자를 위한 안전하고 사용하기 쉬운 환경을 조성하세요.

이 프로젝트의 초기 초안을 개략적으로 설명할 때 간단하고 간단해 보일 수 있으며 어쩌면 주말 안에 완료될 수 있을 것 같은 인상을 줄 수도 있습니다.

초기 체크리스트 초안:

  • OAuth 제공자(GitHub, Google)
  • SAML SSO
  • 안전한 세션 관리
  • RBAC
  • 매직링크 인증

초기 초안이 변경되지 않았다면 프로젝트가 충분한 피드백과 의견을 받지 못한 것입니다. 당연히 목록도 길어졌습니다.

실제 체크리스트:

  • 사용자 자격 증명으로 가입
  • OAuth 제공업체에 가입(Github, Google)
  • 사용자 자격 증명으로 로그인
  • OAuth 제공업체(Github, Google)로 로그인
  • SSO(SAML)로 로그인
  • Novu CLI에서 로그인
  • Vercel Marketplace에서 로그인/업
  • 조직 만들기
  • 조직관리
  • 사용자 관리(사용자 정보, 자격 증명 등 업데이트…)
  • MFA/2FA(SMS/이메일을 통한 OTP, TOTP, 패스키, 생체인식 등)
  • 초대
  • RBAC: 두 가지 역할 관리자 및 편집자
    • admin = 관리자는 웹 플랫폼의 모든 페이지(팀 구성원 및 설정 포함)에 액세스하고 상호 작용할 수 있습니다.
    • 편집자 = 편집자 역할은 여전히 ​​"주 콘텐츠 관리자"(제품 관리자 또는 마케팅 관리자)로 유지됩니다

연구 및 평가

프로젝트 범위를 파악한 후 다음 단계는 연구를 수행하고 원하는 결과를 달성하는 데 필요한 리소스를 평가하는 것입니다.

이 프로세스에는 다음이 포함됩니다.

  • 현재 상태와 제품의 각 레이어를 매우 명확하게 이해하세요.

    • 종속성
    • 엔드포인트
    • 건축
    • 클라이언트 레이어 구성요소 및 표현(프런트엔드)
    • 테스트

    그리고 더 많은 것

  • 이전 사양 개요(내부에 남아 있고 방해되어야 하는 것)

  • 하위 호환성

  • 아마도 이전 동료로부터 유사한 프로젝트에 대한 참고 자료를 찾아보고 그들의 프로세스와 권장 사항을 배워보세요

  • 오픈 소스 솔루션을 찾아보세요

  • 공급업체(타사 솔루션)가 있는지 찾아 비교해 보세요.

그리고 더 많은 것

다른 블로그 게시물에서는 타사 솔루션(또는 제품)을 서비스/인프라로서 서비스 회사로서 평가하고 비교하는 방법을 살펴보겠습니다.

불충분한 연구나 부정확한 평가는 일반적으로 기술적 부채와 향후 리소스 손실(예: 추가 기능 추가 및 유지 관리 시 엔지니어링 시간 등)로 이어지며, 이로 인해 전체를 리팩토링해야 합니다. 따라서 각 옵션의 숨겨진 비용을 검색해 보세요.

숙련된 팀 리더는 각 옵션의 투자 수익(ROI)을 평가하는 방법을 알고 있어 비즈니스에 가장 적합한 결정을 내리는 데 도움이 됩니다.

그래서 우리는 Clerk를 만나게 되었습니다. 이들 솔루션은 대부분의 사용 사례를 포괄하며 비즈니스 관점에서 사용자 및 조직 계층을 관리하기 위해 솔루션을 구현하는 경우 ROI가 합리적입니다.


구현

Novu 서비스에는 다음과 같은 다양한 마이크로 서비스와 측면이 포함되어 있습니다.

  • 알림 채널(SMS, 이메일, 인앱, 푸시, 채팅 등..),
  • 알림 오케스트레이션(기기 간 동기화, 다이제스트 엔진, 지연, 시간대 인식 등..)
  • 알림 관찰 가능성(디버깅, 통찰력 등)
  • 알림 내용 관리(에디터, 브랜딩, 레이아웃, 번역, 변수 관리 등..)
  • 최종 사용자 관리(사용자 기본 설정, 구독자, 주제, 세그먼트, 구독 관리 등..)
  • 계정 관리(SSO, 역할 기반 액세스 제어, 다중 테넌시, 청구 등..)

아래 다이어그램은 Clerk를 구현하기 전 Novu 사용자 및 조직의 인증 및 권한 부여에만 초점을 맞춘 Novu API 구조의 단순화된 버전을 보여줍니다.

Moving User Management from In-House to a Product: Why We Did It and What We Learned

우리는 MongoDB를 사용하여 Novu가 요구하는 모든 데이터, 즉 모든 사용자, 조직, 테넌트, 구독자, 주제 등 모든 것을 저장합니다.

Clerk은 사용자를 관리하기 위한 자체 데이터베이스를 보유하고 있기 때문에 데이터베이스 간의 마이그레이션 및 동기화를 매우 신중하고 정확하게 처리해야 했습니다.


JWT 전략과 사무원 전략

우리가 확인해야 할 주요 사항 중 하나는 Novu를 사용할 때 UserSessionData 개체가 사용자 세션을 중단하지 않도록 변경되지 않는다는 것입니다. 호환성을 유지해야 합니다.

여기에서 jwt.stratgy.ts 파일 예제를 볼 수 있습니다.

//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;
  }
}

Moving User Management from In-House to a Product: Why We Did It and What We Learned

앱의 나머지 부분과의 호환성을 유지하기 위해 JWT 페이로드를 Clerk에서 기존 JWT 형식으로 변환해야 했습니다.

이렇게 했습니다.

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

여기에서 clerk.strategy.ts 파일 예제를 볼 수 있습니다.

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...
}

Moving User Management from In-House to a Product: Why We Did It and What We Learned


Clerk와 Novu 간의 동기화

이상적으로는 Clerk만 사용하여 사용자, 조직 등을 생성하고 검색하는 것이 목표이지만, 안타깝게도 사용자와 조직에 대한 일부 메타데이터를 효율적인 방식으로 저장하고 쿼리해야 하기 때문에 완전히 가능하지는 않습니다.

다음은 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 }
    );
  }

이 방법은 다양한 MongoDB 관련 구성을 사용하여 문서를 필터링합니다. 이는 그러한 쿼리를 위한 데이터베이스가 아니기 때문에 Clerk를 사용하여 성능을 재현하는 것이 불가능합니다.

우리가 할 수 있는 일은 MongoDB 조직 컬렉션에 조직에 대한 이러한 메타데이터를 저장하고 externalId를 사용하여 Clerk 데이터베이스와 컬렉션을 연결/동기화하는 것입니다.

Moving User Management from In-House to a Product: Why We Did It and What We Learned

이제 Clerk와 MongoDB를 결합하여 필요한 경우 메타데이터를 쿼리할 수 있습니다.

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

getUsersMembersOrganizations를 호출하면 findPartnerConfigurationDetails가 필수 조직 데이터를 가져와 CommunityOrganizationRepository에서 필터링된 검색을 수행하여 관련 구성만 반환되도록 합니다.

Clark와 Novu 간에 사용자와 조직만 동기화하면 되며, 조직 구성원은 동기화할 필요가 없습니다.


사용자 및 조직 동기화

데이터베이스 ID를 동기화하는 방법에는 두 가지가 있습니다.

  • middleware - any endpoint in API will sync the IDs if it detects that JWT doesn’t yet contain an internal ID.
  • webhook - as soon as the user/org is registered in Clerk, Clerk calls Novu’s API webhook, and we sync it.

Moving User Management from In-House to a Product: Why We Did It and What We Learned

Here is the flow we had in mind:

  1. A user creates a new account via frontend using the Clerk component
  2. Gets a new JWT containing Clerk user-id
  3. Any request that hits the API triggers the syncing process (given it hasn’t yet happened)
  4. A new user is created in Novu’s MongoDB containing the Clerk’s externalId
  5. Clerk user object gets updated with Novu internal object id (saved as externalId in Clerk)
  6. The new token returned from Clerk now contains an externalId that is equal to Novu's internal user ID.
  7. In the Clerk strategy in validate() function on API - we set _id to equal to externalId so it is compatible with the rest of the app.

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.


What is stored in Clerk vs Novu

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.

Moving User Management from In-House to a Product: Why We Did It and What We Learned


Injection of Enterprise Edition providers

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.


AuthService & AuthModule - dynamic injection

Moving User Management from In-House to a Product: Why We Did It and What We Learned

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.

Repositories - users, organizations, members

Same logic applies for the repositories. No module is being initialized here, they’re just directly injected to the original repository classes.

Moving User Management from In-House to a Product: Why We Did It and What We Learned


Controllers

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 {
    ...
}


Issues with this approach

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

Moving User Management from In-House to a Product: Why We Did It and What We Learned

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 .


Endpoints modification

We modified the endpoints as follows:

  • AuthController: Removed and replaced by frontend calls to Clerk.
  • UserController: Removed, added a sync endpoint for Clerk users with MongoDB.
  • OrganizationController: Removed several endpoints, which can be migrated later.
  • InvitesController: Completely removed.
  • StorageModule: Completely removed.

Key points to consider and avoid

  1. Avoid Storing Frequently Changing Properties in JWT
    • Example: environmentID
    • It can be cumbersome to update these properties.
  2. Simplify Stored Data Structures
    • Avoid storing complex structures in user, organization, or member records.
    • Clerk performs optimally with simple key:value pairs, not arrays of objects.
  3. Implement a User/Organization Replication Mechanism
    • This helps bridge the gap during the migration period before Clerk is fully enabled.
    • Use MongoDB triggers to replicate newly created users and organizations to both Clerk and your internal database.
  4. Store Original Emails
    • Do not sanitize emails as Clerk uses the original email as a unique identifier.

Team Spotlight

Lead Engineer: Adam Chmara

Platform Team Lead: Denis Kralj


Summary

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.

Moving User Management from In-House to a Product: Why We Did It and What We Learned


돌이켜보면 보너스 포인트

사용자 관리를 위해 Clerk를 구현하기로 결정했을 때 Clerk가 향후 지원하고 제공할 기능과 특징을 확장하는 데 따른 장기적인 이점도 선택했습니다.

가까운 미래에 지원을 고려할 수 있는 몇 가지 예는 다음과 같습니다.

  • 세분화된 액세스 제어(FGAC)
    • 속성 기반: FGAC는 사용자, 리소스 및 환경의 다양한 속성을 기반으로 액세스 결정이 이루어지는 ABAC(속성 기반 액세스 제어)를 사용하여 구현되는 경우가 많습니다. 속성에는 사용자 역할, 부서, 리소스 유형, 시간 등이 포함될 수 있습니다.
    • 유연성: FGAC는 상세한 조건 기반 액세스 제어를 허용하여 더 큰 유연성과 세분성을 제공합니다. 이는 매우 구체적인 시나리오에 맞게 권한을 세부적으로 조정할 수 있음을 의미합니다.
    • 동적: FGAC는 시간에 민감한 액세스 또는 위치 기반 제한과 같은 환경 변화에 동적으로 적응할 수 있습니다.
    • 세부 권한: FGAC의 권한은 더 구체적이며 개별 작업, 사용자 또는 상황에 맞게 조정할 수 있습니다.

이러한 수준의 세부적인 유연성을 Novu에 제공하십시오. 구현의 잠재적인 복잡성으로 인해 범위를 벗어났거나 심지어 문턱에서 긁혔을 수도 있습니다.

  • 사용자 명의 도용

    저희 고객 성공 또는 지원 팀은 비밀번호나 기타 인증 세부정보를 알 필요 없이 이를 사용하여 가장한 사용자의 관점에서 문제를 해결하고 지원을 제공하거나 기능을 테스트할 수 있습니다.

    • 사용자 문제 진단 및 해결에 소요되는 시간과 복잡성을 줄입니다.
    • 지원 담당자가 사용자와 똑같이 시스템을 보고 상호 작용할 수 있으므로 지원 또는 관리 조치가 정확한지 확인합니다.
    • 보다 빠르고 효과적인 지원을 제공하여 사용자 만족도를 높입니다.

간단히 말하면, 이제 인증 인프라가 갖춰져 있기 때문에 Novu 사용자의 경험을 쉽게 개선할 수 있을 것입니다.


AuthN(또는 다른 기능)과 관련된 추가 기능을 제안하고 싶다면 로드맵을 방문하여 요청을 검토 및 찬성하거나 아이디어를 제출하세요.


읽은 내용이 마음에 드셨나요? 더 많은 업데이트를 보려면 팔로우를 누르고 아래에 댓글을 남겨주세요. 당신의 ?

를 듣고 싶습니다.
Moving User Management from In-House to a Product: Why We Did It and What We Learned

에밀 피어스

나는 커피숍에서 코드와 단어를 쓴다.

위 내용은 사용자 관리를 사내에서 제품으로 이동: 이를 수행한 이유와 배운 내용의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.