feat: add fim record provider create / search and sign token

pull/100/head
moonrailgun 2 years ago
parent b64d037b60
commit ed1d7cc1d6

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

@ -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';

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

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

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

@ -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';
@ -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 }>) => {
@ -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,
};
},
};
}

@ -14,6 +14,7 @@ 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}`;
@ -21,28 +22,30 @@ export const GithubStrategy: StrategyType = {
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),

@ -1,5 +1,6 @@
export interface StrategyType {
name: string;
type: 'oauth';
checkAvailable: () => boolean;
getUrl: () => string;
getUserInfo: (code: string) => Promise<{

@ -21,6 +21,7 @@ import {
db,
call,
BannedError,
UserStructWithToken,
} from 'tailchat-server-sdk';
import {
generateRandomNumStr,
@ -29,6 +30,7 @@ import {
} from '../../../lib/utils';
import type { TFunction } from 'i18next';
import _ from 'lodash';
import type { UserStruct } from 'tailchat-server-sdk';
const { isValidObjectId, Types } = db;
@ -89,6 +91,12 @@ class UserService extends TcService {
emailOTP: { type: 'string', optional: true },
},
});
this.registerAction('signUserToken', this.signUserToken, {
visibility: 'public',
params: {
userId: 'string',
},
});
this.registerAction('modifyPassword', this.modifyPassword, {
rest: 'POST /modifyPassword',
params: {
@ -185,6 +193,12 @@ class UserService extends TcService {
},
},
});
this.registerAction('findUserByEmail', this.findUserByEmail, {
visibility: 'public',
params: {
email: 'string',
},
});
this.registerAction('updateUserField', this.updateUserField, {
params: {
fieldName: 'string',
@ -229,13 +243,13 @@ class UserService extends TcService {
},
});
this.registerAction('generateUserToken', this.generateUserToken, {
visibility: 'public',
params: {
userId: 'string',
nickname: 'string',
email: 'string',
avatar: 'string',
},
visibility: 'public',
});
this.registerAuthWhitelist([
@ -402,7 +416,7 @@ class UserService extends TcService {
},
any
>
) {
): Promise<UserStructWithToken> {
const params = { ...ctx.params };
const t = ctx.meta.t;
await this.validateEntity(params);
@ -452,6 +466,28 @@ class UserService extends TcService {
return json;
}
/**
* token
*
*/
async signUserToken(
ctx: TcContext<{
userId: string;
}>
): Promise<string> {
const userId = ctx.params.userId;
const userInfo = await call(ctx).getUserInfo(userId);
const token = this.generateJWT({
_id: userInfo._id,
nickname: userInfo.nickname,
email: userInfo.email,
avatar: userInfo.avatar,
});
return token;
}
/**
*
*/
@ -801,6 +837,29 @@ class UserService extends TcService {
return list;
}
/**
*
*/
async findUserByEmail(
ctx: TcContext<{
email: string;
}>
): Promise<UserStruct | null> {
const email = ctx.params.email;
const doc = await this.adapter.model.findOne({
email,
});
if (!doc) {
return null;
}
const user = await this.transformDocuments(ctx, {}, doc);
return user;
}
/**
*
*/

Loading…
Cancel
Save