Skip to content

Commit

Permalink
feat: add fim record provider create / search and sign token
Browse files Browse the repository at this point in the history
  • Loading branch information
moonrailgun committed Jun 28, 2023
1 parent b64d037 commit ed1d7cc
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 83 deletions.
35 changes: 35 additions & 0 deletions packages/types/model/user.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,45 @@
export const userType = ['normalUser', 'pluginBot', 'openapiBot'] as const;
export type UserType = (typeof userType)[number];

export interface UserBaseInfo {
_id: string;
/**
* Username cannot modify
*
* There must be one with email
*/
username?: string;

/**
* E-mail cannot be modified
* required
*/
email: string;
/**
* display name that can be modified
*/
nickname: string;
/**
* Identifier, together with username constitutes a globally unique username
* use for search
* <username>#<discriminator>
*/
discriminator: string;
avatar: string | null;
/**
* Is it a temporary user
* @default false
*/
temporary: boolean;
type: UserType;
emailVerified: boolean;
extra?: Record<string, unknown>;
}

export interface UserInfoWithPassword extends UserBaseInfo {
password: string;
}

export interface UserInfoWithToken extends UserBaseInfo {
token: string;
}
2 changes: 1 addition & 1 deletion server/packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type {
} from './structs/group';
export { GroupPanelType } from './structs/group';
export { userType } from './structs/user';
export type { UserStruct, UserType } from './structs/user';
export type { UserStruct, UserType, UserStructWithToken } from './structs/user';

// db
export * as db from './db';
Expand Down
6 changes: 6 additions & 0 deletions server/packages/sdk/src/services/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,9 @@ export class ServiceUnavailableError extends TcError {
super('Service unavailable', 503, 'SERVICE_NOT_AVAILABLE', data);
}
}

export class NotFoundError extends TcError {
constructor(data?: unknown) {
super('Not found', 404, 'NOT_FOUND', data);
}
}
56 changes: 9 additions & 47 deletions server/packages/sdk/src/structs/user.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,9 @@
export const userType = ['normalUser', 'pluginBot', 'openapiBot'] as const;
export type UserType = (typeof userType)[number];

export interface UserStruct {
_id: string;

/**
* 用户名 不可被修改
* 与email必有一个
*/
username?: string;

/**
* 邮箱 不可被修改
* 必填
*/
email: string;

password: string;

/**
* 可以被修改的显示名
*/
nickname: string;

/**
* 识别器, 跟username构成全局唯一的用户名
* 用于搜索
* <username>#<discriminator>
*/
discriminator: string;

/**
* 是否为临时用户
* @default false
*/
temporary: boolean;

/**
* 头像
*/
avatar?: string;

type: UserType;

emailVerified: boolean;
}
import type { UserBaseInfo, UserInfoWithToken, UserType } from 'tailchat-types';
import { userType } from 'tailchat-types';

export {
userType,
UserType,
UserBaseInfo as UserStruct,
UserInfoWithToken as UserStructWithToken,
};
17 changes: 17 additions & 0 deletions server/plugins/com.msgbyte.fim/models/fim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ const { getModelForClass, prop, modelOptions, TimeStamps } = db;
export class Fim extends TimeStamps implements db.Base {
_id: db.Types.ObjectId;
id: string;

/**
* 账号供应商
* 如 Github
*/
@prop()
provider: string;

/**
* 在账号供应商那边的唯一标识
* 根据不同的供应商有不同的格式
*/
@prop()
providerId: string;

@prop()
userId: string;
}

export type FimDocument = db.DocumentType<Fim>;
Expand Down
93 changes: 78 additions & 15 deletions server/plugins/com.msgbyte.fim/services/fim.service.dev.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TcService, TcDbService, TcPureContext } from 'tailchat-server-sdk';
import { TcService, TcDbService, TcPureContext, db } from 'tailchat-server-sdk';
import type { UserStructWithToken } from 'tailchat-server-sdk/src/structs/user';
import type { FimDocument, FimModel } from '../models/fim';
import { strategies } from '../strategies';
import type { StrategyType } from '../strategies/types';
Expand All @@ -15,23 +16,39 @@ class FimService extends TcService {
}

onInit() {
// this.registerLocalDb(require('../models/fim').default);

strategies
.filter((strategy) => strategy.checkAvailable())
.map((strategy) => {
const action = this.buildStrategyAction(strategy);
const name = strategy.name;
this.registerAction(`${name}.url`, action.url);
this.registerAction(`${name}.redirect`, action.redirect);

this.registerAuthWhitelist([`/${name}/url`, `/${name}/redirect`]);
});
this.registerLocalDb(require('../models/fim').default);

const availableStrategies = strategies.filter((strategy) =>
strategy.checkAvailable()
);

this.registerAction('availableStrategies', () => {
return availableStrategies.map((s) => ({
name: s.name,
type: s.type,
}));
});

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 {
url: async (ctx: TcPureContext) => {
loginUrl: async (ctx: TcPureContext) => {
return strategy.getUrl();
},
redirect: async (ctx: TcPureContext<{ code: string }>) => {
Expand All @@ -41,7 +58,53 @@ class FimService extends TcService {
throw new Error(JSON.stringify(ctx.params));
}

return strategy.getUserInfo(code);
const providerUserInfo = await strategy.getUserInfo(code);

const fimRecord = await this.adapter.model.findOne({
provider: strategyName,
providerId: providerUserInfo.id,
});

if (!!fimRecord) {
// 存在记录,直接签发 token
const token = await ctx.call('user.signUserToken', {
userId: fimRecord.userId,
});

return { type: 'token', token };
}

// 不存在记录,查找是否已经注册过,如果已经注册过需要绑定,如果没有注册过则创建账号并绑定用户关系
const userInfo = await ctx.call('user.findUserByEmail', {
email: providerUserInfo.email,
});
if (!!userInfo) {
// 用户已存在,需要登录后才能确定绑定关系
return {
type: 'existed',
};
}

const newUserInfo: UserStructWithToken = await ctx.call(
'user.register',
{
email: providerUserInfo.email,
nickname: providerUserInfo.nickname,
password: new db.Types.ObjectId(), // random password
}
);

await this.adapter.model.create({
provider: strategyName,
providerId: providerUserInfo.id,
userId: String(newUserInfo._id),
});

return {
type: 'token',
isNew: true,
token: newUserInfo.token,
};
},
};
}
Expand Down
39 changes: 21 additions & 18 deletions server/plugins/com.msgbyte.fim/strategies/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,38 @@ const redirect_uri = `${config.apiUrl}/api/plugin:com.msgbyte.fim/github/redirec

export const GithubStrategy: StrategyType = {
name: 'github',
type: 'oauth',
checkAvailable: () => !!clientInfo.id && !!clientInfo.secret,
getUrl: () => {
return `${authorize_uri}?client_id=${clientInfo.id}&redirect_uri=${redirect_uri}`;
},
getUserInfo: async (code) => {
console.log('authorization code:', code);

const tokenResponse = await got(access_token_uri, {
method: 'POST',
searchParams: {
client_id: clientInfo.id,
client_secret: clientInfo.secret,
code: code,
},
headers: {
accept: 'application/json',
},
}).json<{ access_token: string }>();
const tokenResponse = await got
.post(access_token_uri, {
searchParams: {
client_id: clientInfo.id,
client_secret: clientInfo.secret,
code: code,
},
headers: {
accept: 'application/json',
},
})
.json<{ access_token: string }>();

const accessToken = tokenResponse.access_token;
console.log(`access token: ${accessToken}`);

const result = await got(userinfo_uri, {
method: 'GET',
headers: {
accept: 'application/json',
Authorization: `token ${accessToken}`,
},
}).json<{ id: number; name: string; email: string; avatar_url: string }>();
const result = await got
.get(userinfo_uri, {
headers: {
accept: 'application/json',
Authorization: `token ${accessToken}`,
},
})
.json<{ id: number; name: string; email: string; avatar_url: string }>();

return {
id: String(result.id),
Expand Down
1 change: 1 addition & 0 deletions server/plugins/com.msgbyte.fim/strategies/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface StrategyType {
name: string;
type: 'oauth';
checkAvailable: () => boolean;
getUrl: () => string;
getUserInfo: (code: string) => Promise<{
Expand Down
Loading

0 comments on commit ed1d7cc

Please sign in to comment.