Skip to content

Commit

Permalink
💄 style: support webhooks for casdoor (lobehub#3942)
Browse files Browse the repository at this point in the history
* ✨ feat: Support Casdoor provider

* ✨ feat: + webhook for casdoor

* 🐛 fix: skip test
  • Loading branch information
cy948 authored Sep 20, 2024
1 parent ea28c41 commit 1f2f6a5
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 1 deletion.
60 changes: 60 additions & 0 deletions src/app/api/webhooks/casdoor/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest';

interface User {
name: string;
id: string;
type: 'normal-user' | 'admin' | 'super-admin';
displayName: string;
firstName: string;
lastName: string;
avatar: string;
email: string;
emailVerified: boolean;
}

interface UserDataUpdatedEvent {
user: string; // 用户名
action: 'update-user';
extendedUser: User; // 扩展用户信息
}

const userDataUpdatedEvent: UserDataUpdatedEvent = {
user: 'admin',
action: 'update-user',
extendedUser: {
name: 'admin',
id: '35edace3-00c6-41e1-895e-97c519b1d8cc',
type: 'normal-user',
displayName: 'Admin',
firstName: '',
lastName: '',
avatar: 'https://cdn.casbin.org/img/casbin.svg',
email: 'admin@example.cn',
emailVerified: false,
},
};

const AUTH_CASDOOR_WEBHOOK_SECRET = 'casdoor-secret';

// Test Casdoor Webhooks in Local dev, here is some tips:
// - Replace the var `AUTH_CASDOOR_WETHOOK_SECRET` with the actual value in your `.env` file
// - Start web request: If you want to run the test, replace `describe.skip` with `describe` below
// - Run this test with command:
// pnpm vitest --run --testNamePattern='^ ?Test Casdoor Webhooks in Local dev' src/app/api/webhooks/casdoor/__tests__/route.test.ts

describe.skip('Test Casdoor Webhooks in Local dev', () => {
// describe('Test Casdoor Webhooks in Local dev', () => {
it('should send a POST request with casdoor headers', async () => {
const url = 'http://localhost:3010/api/webhooks/casdoor'; // 替换为目标URL
const data = userDataUpdatedEvent;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'casdoor-secret': AUTH_CASDOOR_WEBHOOK_SECRET,
},
body: JSON.stringify(data),
});
expect(response.status).toBe(200); // 检查响应状态
});
});
40 changes: 40 additions & 0 deletions src/app/api/webhooks/casdoor/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NextResponse } from 'next/server';

import { authEnv } from '@/config/auth';
import { pino } from '@/libs/logger';
import { NextAuthUserService } from '@/server/services/nextAuthUser';

import { validateRequest } from './validateRequest';

export const POST = async (req: Request): Promise<NextResponse> => {
const payload = await validateRequest(req, authEnv.CASDOOR_WEBHOOK_SECRET);

if (!payload) {
return NextResponse.json(
{ error: 'webhook verification failed or payload was malformed' },
{ status: 400 },
);
}

const { action, extendedUser } = payload;

pino.trace(`casdoor webhook payload: ${{ action, extendedUser }}`);

const nextAuthUserService = new NextAuthUserService();
switch (action) {
case 'update-user': {
return nextAuthUserService.safeUpdateUser(extendedUser.id, {
avatar: extendedUser?.avatar,
email: extendedUser?.email,
fullName: extendedUser.displayName,
});
}

default: {
pino.warn(
`${req.url} received event type "${action}", but no handler is defined for this type`,
);
return NextResponse.json({ error: `unrecognised payload type: ${action}` }, { status: 400 });
}
}
};
38 changes: 38 additions & 0 deletions src/app/api/webhooks/casdoor/validateRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { headers } from 'next/headers';

import { authEnv } from '@/config/auth';

export type CasdoorUserEntity = {
avatar?: string;
displayName: string;
email?: string;
id: string;
};

interface CasdoorWebhookPayload {
action: string;
// Only support user event currently
extendedUser: CasdoorUserEntity;
}

export const validateRequest = async (request: Request, secret?: string) => {
const payloadString = await request.text();
const headerPayload = headers();
const casdoorSecret = headerPayload.get('casdoor-secret')!;
try {
if (casdoorSecret === secret) {
return JSON.parse(payloadString) as CasdoorWebhookPayload;
} else {
console.warn(
'[Casdoor]: secret verify failed, please check your secret in `CASDOOR_WEBHOOK_SECRET`',
);
return;
}
} catch (e) {
if (!authEnv.CASDOOR_WEBHOOK_SECRET) {
throw new Error('`CASDOOR_WEBHOOK_SECRET` environment variable is missing.');
}
console.error('[Casdoor]: incoming webhook failed in verification.\n', e);
return;
}
};
6 changes: 6 additions & 0 deletions src/config/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ export const getAuthConfig = () => {
LOGTO_CLIENT_SECRET: z.string().optional(),
LOGTO_ISSUER: z.string().optional(),
LOGTO_WEBHOOK_SIGNING_KEY: z.string().optional(),

// Casdoor
CASDOOR_WEBHOOK_SECRET: z.string().optional(),
},

runtimeEnv: {
Expand Down Expand Up @@ -259,6 +262,9 @@ export const getAuthConfig = () => {
LOGTO_CLIENT_SECRET: process.env.LOGTO_CLIENT_SECRET,
LOGTO_ISSUER: process.env.LOGTO_ISSUER,
LOGTO_WEBHOOK_SIGNING_KEY: process.env.LOGTO_WEBHOOK_SIGNING_KEY,

// Casdoor
CASDOOR_WEBHOOK_SECRET: process.env.CASDOOR_WEBHOOK_SECRET,
},
});
};
Expand Down
49 changes: 49 additions & 0 deletions src/libs/next-auth/sso-providers/casdoor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { OIDCConfig, OIDCUserConfig } from '@auth/core/providers';

import { CommonProviderConfig } from './sso.config';

interface CasdoorProfile extends Record<string, any> {
avatar: string;
displayName: string;
email: string;
emailVerified: boolean;
firstName: string;
id: string;
lastName: string;
name: string;
owner: string;
permanentAvatar: string;
}

function LobeCasdoorProvider(config: OIDCUserConfig<CasdoorProfile>): OIDCConfig<CasdoorProfile> {
return {
...CommonProviderConfig,
...config,
id: 'casdoor',
name: 'Casdoor',
profile(profile) {
return {
email: profile.email,
emailVerified: profile.emailVerified ? new Date() : null,
image: profile.avatar,
name: profile.displayName ?? profile.firstName ?? profile.lastName,
providerAccountId: profile.id,
};
},
type: 'oidc',
};
}

const provider = {
id: 'casdoor',
provider: LobeCasdoorProvider({
authorization: {
params: { scope: 'openid profile email' },
},
clientId: process.env.AUTH_CASDOOR_ID,
clientSecret: process.env.AUTH_CASDOOR_SECRET,
issuer: process.env.AUTH_CASDOOR_ISSUER,
}),
};

export default provider;
14 changes: 13 additions & 1 deletion src/libs/next-auth/sso-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@ import Auth0 from './auth0';
import Authelia from './authelia';
import Authentik from './authentik';
import AzureAD from './azure-ad';
import Casdoor from './casdoor';
import CloudflareZeroTrust from './cloudflare-zero-trust';
import GenericOIDC from './generic-oidc';
import Github from './github';
import Logto from './logto';
import Zitadel from './zitadel';

export const ssoProviders = [Auth0, Authentik, AzureAD, GenericOIDC, Github, Zitadel, Authelia, Logto, CloudflareZeroTrust];
export const ssoProviders = [
Auth0,
Authentik,
AzureAD,
GenericOIDC,
Github,
Zitadel,
Authelia,
Logto,
CloudflareZeroTrust,
Casdoor,
];

0 comments on commit 1f2f6a5

Please sign in to comment.