You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailchat/server/plugins/com.msgbyte.iam/services/iam.service.ts

158 lines
4.3 KiB
TypeScript

import {
TcService,
TcDbService,
TcPureContext,
db,
UserStructWithToken,
} from 'tailchat-server-sdk';
import { isValidStaticAssetsUrl } from '../../../lib/utils';
import type { IAMDocument, IAMModel } from '../models/iam';
import { strategies } from '../strategies';
import type { StrategyType } from '../strategies/types';
/**
* Federated Identity Management
*
* Unified identity authentication
*/
interface IAMService extends TcService, TcDbService<IAMDocument, IAMModel> {}
class IAMService extends TcService {
get serviceName() {
return 'plugin:com.msgbyte.iam';
}
onInit() {
this.registerLocalDb(require('../models/iam').default);
const availableStrategies = strategies.filter((strategy) =>
strategy.checkAvailable()
);
this.registerAction('availableStrategies', () => {
return availableStrategies.map((s) => ({
name: s.name,
type: s.type,
icon: s.icon,
}));
});
availableStrategies.forEach((strategy) => {
const action = this.buildStrategyAction(strategy);
const strategyName = strategy.name;
this.registerAction(`${strategyName}.loginUrl`, action.loginUrl);
this.registerAction(`${strategyName}.redirect`, action.redirect);
this.registerAuthWhitelist([
`/${strategyName}/loginUrl`,
`/${strategyName}/redirect`,
]);
});
this.registerAuthWhitelist(['/availableStrategies']);
}
buildStrategyAction(strategy: StrategyType) {
const strategyName = strategy.name;
return {
loginUrl: async (ctx: TcPureContext) => {
return strategy.getUrl();
},
redirect: async (ctx: TcPureContext<{ code: string }>) => {
const code = ctx.params.code;
if (!code) {
throw new Error(JSON.stringify(ctx.params));
}
const providerUserInfo = await strategy.getUserInfo(code);
const iamRecord = await this.adapter.model.findOne({
provider: strategyName,
providerId: providerUserInfo.id,
});
if (!!iamRecord) {
// 存在记录,直接签发 token
const token = await ctx.call('user.signUserToken', {
userId: iamRecord.userId,
});
return generatePostMessageHtml({
type: 'token',
token: token,
});
}
const username = providerUserInfo.username;
const email =
providerUserInfo.email ||
`${username}.${strategyName}@iam.msgbyte.com`;
// 不存在记录,查找是否已经注册过,如果已经注册过需要绑定,如果没有注册过则创建账号并绑定用户关系
const userInfo = await ctx.call('user.findUserByEmail', {
email,
});
if (!!userInfo) {
// 用户已存在,需要登录后才能确定绑定关系
return generatePostMessageHtml({ type: 'existed' });
}
let avatar = providerUserInfo.avatar;
if (avatar && isValidStaticAssetsUrl(avatar)) {
try {
const { url } = (await ctx.call(
'file.saveFileWithUrl',
{
fileUrl: avatar,
},
{
timeout: 3000,
}
)) as { url: string };
avatar = url;
} catch (err) {
this.logger.error('Cannot storage avatar', avatar, err);
}
}
const newUserInfo: UserStructWithToken = await ctx.call(
'user.register',
{
email,
nickname: providerUserInfo.nickname,
password: String(new db.Types.ObjectId()), // random password
avatar,
}
);
await this.adapter.model.create({
provider: strategyName,
providerId: providerUserInfo.id,
userId: String(newUserInfo._id),
});
return generatePostMessageHtml({
type: 'token',
isNew: true,
token: newUserInfo.token,
});
},
};
}
}
function generatePostMessageHtml(obj: Record<string, any>) {
const IAM_FE_URL = process.env.IAM_FE_URL || '*';
return {
__raw: true,
html: `<script>window.opener.postMessage(${JSON.stringify({
source: 'plugin:com.msgbyte.iam',
...obj,
})}, "${IAM_FE_URL}");</script><div>Waiting for main window close</div>`,
};
}
export default IAMService;