remove mark

This commit is contained in:
2025-12-04 10:31:37 +08:00
parent 46aa293cce
commit 2a55f2d3ef
35 changed files with 1837 additions and 726 deletions

11
src/auth/index.ts Normal file
View File

@@ -0,0 +1,11 @@
/**
* 可以不需要user成功, 有则赋值,交给下一个中间件
*/
export const authCan = 'auth-can';
/**
* 必须需要user成功
*/
export const auth = 'auth';
export * from './models/index.ts';

View File

@@ -0,0 +1,81 @@
import { User } from '../models/user.ts';
import http from 'node:http';
import cookie from 'cookie';
export const error = (msg: string, code = 500) => {
return JSON.stringify({ code, message: msg });
};
type CheckAuthOptions = {
check401?: boolean; // 是否返回权限信息
};
/**
* 手动验证token如果token不存在则返回401
* @param req
* @param res
* @returns
*/
export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse, opts?: CheckAuthOptions) => {
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
const url = new URL(req.url || '', 'http://localhost');
const check401 = opts?.check401 ?? true; // 是否返回401错误
const resNoPermission = () => {
res.statusCode = 401;
res.end(error('Invalid authorization'));
return { tokenUser: null, token: null, hasToken: false };
};
if (!token) {
token = url.searchParams.get('token') || '';
}
if (!token) {
const parsedCookies = cookie.parse(req.headers.cookie || '');
token = parsedCookies.token || '';
}
if (!token && check401) {
return resNoPermission();
}
if (token) {
token = token.replace('Bearer ', '');
}
let tokenUser;
const hasToken = !!token; // 是否有token存在
try {
tokenUser = await User.verifyToken(token);
} catch (e) {
console.log('checkAuth error', e);
res.statusCode = 401;
res.end(error('Invalid token'));
return { tokenUser: null, token: null, hasToken: false };
}
return { tokenUser, token, hasToken };
};
/**
* 获取登录用户有则获取无则返回null
* @param req
* @returns
*/
export const getLoginUser = async (req: http.IncomingMessage) => {
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
const url = new URL(req.url || '', 'http://localhost');
if (!token) {
token = url.searchParams.get('token') || '';
}
if (!token) {
const parsedCookies = cookie.parse(req.headers.cookie || '');
token = parsedCookies.token || '';
}
if (token) {
token = token.replace('Bearer ', '');
}
if (!token) {
return null;
}
let tokenUser;
try {
tokenUser = await User.verifyToken(token);
return { tokenUser, token };
} catch (e) {
return null;
}
};

View File

@@ -0,0 +1,56 @@
import { User } from '../models/user.ts';
import type { App } from '@kevisual/router';
/**
* 添加auth中间件, 用于验证token
* 添加 id: auth 必须需要user成功
* 添加 id: auth-can 可以不需要user成功有则赋值
*
* @param app
*/
export const addAuth = (app: App) => {
app
.route({
path: 'auth',
id: 'auth',
})
.define(async (ctx) => {
const token = ctx.query.token;
if (!token) {
app.throw(401, 'Token is required');
}
const user = await User.getOauthUser(token);
if (!user) {
app.throw(401, 'Token is invalid');
}
if (ctx.state) {
ctx.state.tokenUser = user;
} else {
ctx.state = {
tokenUser: user,
};
}
})
.addTo(app);
app
.route({
path: 'auth',
key: 'can',
id: 'auth-can',
})
.define(async (ctx) => {
if (ctx.query?.token) {
const token = ctx.query.token;
const user = await User.getOauthUser(token);
if (ctx.state) {
ctx.state.tokenUser = user;
} else {
ctx.state = {
tokenUser: user,
};
}
}
})
.addTo(app);
};

3
src/auth/models/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { User, UserInit, UserServices, UserModel } from './user.ts';
export { UserSecretInit, UserSecret } from './user-secret.ts';
export { OrgInit, Org } from './org.ts';

184
src/auth/models/org.ts Normal file
View File

@@ -0,0 +1,184 @@
import { DataTypes, Model, Op, Sequelize } from 'sequelize';
import { useContextKey } from '@kevisual/context';
import { SyncOpts, User } from './user.ts';
type AddUserOpts = {
role: string;
};
export enum OrgRole {
admin = 'admin',
member = 'member',
owner = 'owner',
}
export class Org extends Model {
declare id: string;
declare username: string;
declare description: string;
declare users: { role: string; uid: string }[];
/**
* operateId 是真实操作者的id
* @param user
* @param opts
*/
async addUser(user: User, opts?: { operateId?: string; role: string; needPermission?: boolean; isAdmin?: boolean }) {
const hasUser = this.users.find((u) => u.uid === user.id);
if (hasUser) {
return;
}
if (user.type !== 'user') {
throw Error('Only user can be added to org');
}
if (opts?.needPermission) {
if (opts?.isAdmin) {
} else {
const adminUsers = this.users.filter((u) => u.role === 'admin' || u.role === 'owner');
const adminIds = adminUsers.map((u) => u.uid);
const hasPermission = adminIds.includes(opts.operateId);
if (!hasPermission) {
throw Error('No permission');
}
}
}
try {
await user.expireOrgs();
} catch (e) {
console.error('expireOrgs', e);
}
const users = [...this.users];
if (opts?.role === 'owner') {
const orgOwner = users.find((u) => u.role === 'owner');
if (opts.isAdmin) {
} else {
if (!opts.operateId) {
throw Error('operateId is required');
}
const owner = await User.findByPk(opts?.operateId);
if (!owner) {
throw Error('operateId is not found');
}
if (orgOwner?.uid !== owner.id) {
throw Error('No permission');
}
}
if (orgOwner) {
orgOwner.role = 'admin';
}
users.push({ role: 'owner', uid: user.id });
} else {
users.push({ role: opts?.role || 'member', uid: user.id });
}
await Org.update({ users }, { where: { id: this.id } });
}
/**
* operateId 是真实操作者的id
* @param user
* @param opts
*/
async removeUser(user: User, opts?: { operateId?: string; needPermission?: boolean; isAdmin?: boolean }) {
if (opts?.needPermission) {
if (opts?.isAdmin) {
} else {
const adminUsers = this.users.filter((u) => u.role === 'admin' || u.role === 'owner');
const adminIds = adminUsers.map((u) => u.uid);
const hasPermission = adminIds.includes(opts.operateId);
if (!hasPermission) {
throw Error('No permission');
}
}
}
await user.expireOrgs();
const users = this.users.filter((u) => u.uid !== user.id || u.role === 'owner');
await Org.update({ users }, { where: { id: this.id } });
}
/**
* operateId 是真实操作者的id
* @param user
* @param opts
*/
async getUsers(opts?: { operateId: string; needPermission?: boolean; isAdmin?: boolean }) {
const usersIds = this.users.map((u) => u.uid);
const orgUser = this.users;
if (opts?.needPermission) {
// 不在组织内或者不是管理员,如果需要权限,返回空
if (opts.isAdmin) {
} else {
const hasPermission = usersIds.includes(opts.operateId);
if (!hasPermission) {
return {
hasPermission: false,
users: [],
};
}
}
}
const _users = await User.findAll({
where: {
id: {
[Op.in]: usersIds,
},
},
});
const users = _users.map((u) => {
const role = orgUser.find((r) => r.uid === u.id)?.role;
return {
id: u.id,
username: u.username,
role: role,
};
});
return { users };
}
/**
* 检测用户是否在组织内且角色为role
* @param user
* @param opts
*/
async getInRole(userId: string, role = 'admin') {
const user = this.users.find((u) => u.uid === userId && u.role === role);
return !!user;
}
}
/**
* 组织模型在sequelize之后初始化
*/
export const OrgInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => {
const sequelize = useContextKey<Sequelize>('sequelize');
Org.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
description: {
type: DataTypes.STRING,
allowNull: true,
},
users: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: [],
},
},
{
sequelize: newSequelize || sequelize,
modelName: tableName || 'cf_org',
paranoid: true,
},
);
if (sync) {
await Org.sync({ alter: true, logging: false, ...sync }).catch((e) => {
console.error('Org sync', e);
});
return Org;
}
return Org;
};
export const OrgModel = useContextKey('OrgModel', () => Org);

View File

@@ -0,0 +1,261 @@
import { DataTypes, Model, Sequelize } from 'sequelize';
import { useContextKey } from '@kevisual/context';
import { Redis } from 'ioredis';
import { SyncOpts, User } from './user.ts';
import { oauth } from '../oauth/auth.ts';
import { OauthUser } from '../oauth/oauth.ts';
export const redis = useContextKey<Redis>('redis');
const UserSecretStatus = ['active', 'inactive', 'expired'] as const;
type Data = {
[key: string]: any;
/**
* 微信开放平台的某一个应用的openid
*/
wxOpenid?: string;
/**
* 微信开放平台的unionid主要
*/
wxUnionid?: string;
/**
* 微信公众号的openid次要
*/
wxmpOpenid?: string;
}
export class UserSecret extends Model {
static oauth = oauth;
declare id: string;
declare token: string;
declare userId: string;
declare orgId: string;
declare title: string;
declare description: string;
declare status: (typeof UserSecretStatus)[number];
declare expiredTime: Date;
declare data: Data;
/**
* 验证token
* @param token
* @returns
*/
static async verifyToken(token: string) {
if (!oauth.isSecretKey(token)) {
return await oauth.verifyToken(token);
}
// const secretToken = await oauth.verifyToken(token);
// if (secretToken) {
// return secretToken;
// }
const userSecret = await UserSecret.findOne({
where: { token },
});
if (!userSecret) {
return null; // 如果没有找到对应的用户密钥则返回null
}
if (userSecret.isExpired()) {
return null; // 如果用户密钥已过期则返回null
}
if (userSecret.status !== 'active') {
return null; // 如果用户密钥状态不是active则返回null
}
// 如果用户密钥未过期,则返回用户信息
const oauthUser = await userSecret.getOauthUser();
if (!oauthUser) {
return null; // 如果没有找到对应的oauth用户则返回null
}
// await oauth.saveSecretKey(oauthUser, userSecret.token);
// 存储到oauth中的token store中
return oauthUser;
}
/**
* owner 组织用户的 oauthUser
* @returns
*/
async getOauthUser() {
const user = await User.findOne({
where: { id: this.userId },
attributes: ['id', 'username', 'type', 'owner'],
});
let org: User = null;
if (!user) {
return null; // 如果没有找到对应的用户则返回null
}
const expiredTime = this.expiredTime ? new Date(this.expiredTime).getTime() : null;
const oauthUser: Partial<OauthUser> = {
id: user.id,
username: user.username,
type: 'user',
oauthExpand: {
expiredTime: expiredTime,
},
};
if (this.orgId) {
org = await User.findOne({
where: { id: this.orgId },
attributes: ['id', 'username', 'type', 'owner'],
});
if (org) {
oauthUser.id = org.id;
oauthUser.username = org.username;
oauthUser.type = 'org';
oauthUser.uid = user.id;
} else {
console.warn(`getOauthUser: org not found for orgId ${this.orgId}`);
}
}
return oauth.getOauthUser(oauthUser);
}
isExpired() {
if (!this.expiredTime) {
return false; // 没有设置过期时间
}
const now = Date.now();
const expiredTime = new Date(this.expiredTime);
return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
}
async createNewToken() {
if (this.token) {
await oauth.delToken(this.token);
}
const token = await UserSecret.createToken();
this.token = token;
await this.save();
return token;
}
static async createToken() {
let token = oauth.generateSecretKey();
// 确保生成的token是唯一的
while (await UserSecret.findOne({ where: { token } })) {
token = oauth.generateSecretKey();
}
return token;
}
static async createSecret(tokenUser: { id: string; uid?: string }, expireDay = 365) {
const expireTime = expireDay * 24 * 60 * 60 * 1000; // 转换为毫秒
const token = await UserSecret.createToken();
let userId = tokenUser.id;
let orgId: string = null;
if (tokenUser.uid) {
userId = tokenUser.uid;
orgId = tokenUser.id; // 如果是组织用户则uid是组织ID
}
const userSecret = await UserSecret.create({
userId,
orgId,
token,
expiredTime: new Date(Date.now() + expireTime),
});
return userSecret;
}
async getPermission(opts: { id: string; uid?: string }) {
const { id, uid } = opts;
let userId: string = id;
let hasPermission = false;
let isUser = false;
let isAdmin: boolean = null;
if (uid) {
userId = uid;
}
if (!id) {
return {
hasPermission,
isUser,
isAdmin,
};
}
if (this.userId === userId) {
hasPermission = true;
isUser = true;
}
if (hasPermission) {
return {
hasPermission,
isUser,
isAdmin,
};
}
if (this.orgId) {
const orgUser = await User.findByPk(this.orgId);
if (orgUser && orgUser.owner === userId) {
isAdmin = true;
hasPermission = true;
}
}
return {
hasPermission,
isUser,
isAdmin,
};
}
}
/**
* 组织模型在sequelize之后初始化
*/
export const UserSecretInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => {
const sequelize = useContextKey<Sequelize>('sequelize');
UserSecret.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
status: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: 'active',
comment: '状态',
},
title: {
type: DataTypes.TEXT,
allowNull: true,
},
expiredTime: {
type: DataTypes.DATE,
allowNull: true,
},
token: {
type: DataTypes.STRING,
allowNull: false,
comment: '用户密钥',
defaultValue: '',
},
userId: {
type: DataTypes.UUID,
allowNull: true,
},
data: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: {},
},
orgId: {
type: DataTypes.UUID,
allowNull: true,
comment: '组织ID',
},
},
{
sequelize: newSequelize || sequelize,
modelName: tableName || 'cf_user_secret',
},
);
if (sync) {
await UserSecret.sync({ alter: true, logging: false, ...sync }).catch((e) => {
console.error('UserSecret sync', e);
});
return UserSecret;
}
return UserSecret;
};
export const UserSecretModel = useContextKey('UserSecretModel', () => UserSecret);

370
src/auth/models/user.ts Normal file
View File

@@ -0,0 +1,370 @@
import { DataTypes, Model, Op, Sequelize } from 'sequelize';
import { nanoid, customAlphabet } from 'nanoid';
import { CustomError } from '@kevisual/router';
import { Org } from './org.ts';
import { useContextKey } from '@kevisual/context';
import { Redis } from 'ioredis';
import { oauth } from '../oauth/auth.ts';
import { cryptPwd } from '../oauth/salt.ts';
import { OauthUser } from '../oauth/oauth.ts';
export const redis = useContextKey<Redis>('redis');
import { UserSecret } from './user-secret.ts';
type UserData = {
orgs?: string[];
wxUnionId?: string;
phone?: string;
};
export enum UserTypes {
'user' = 'user',
'org' = 'org',
'visitor' = 'visitor',
}
/**
* 用户模型,在sequelize和Org之后初始化
*/
export class User extends Model {
static oauth = oauth;
declare id: string;
declare username: string;
declare nickname: string; // 昵称
declare password: string;
declare salt: string;
declare needChangePassword: boolean;
declare description: string;
declare data: UserData;
declare type: string; // user | org | visitor
declare owner: string;
declare orgId: string;
declare email: string;
declare avatar: string;
tokenUser: any;
setTokenUser(tokenUser: any) {
this.tokenUser = tokenUser;
}
/**
* uid 是用于 orgId 的用户id, 如果uid存在则表示是用户是组织其中uid为真实用户
* @param uid
* @returns
*/
async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week', expand: any = {}) {
const { id, username, type } = this;
const oauthUser: OauthUser = {
id,
username,
uid,
userId: uid || id, // 必存在真实用户id
type: type as 'user' | 'org',
};
if (uid) {
oauthUser.orgId = id;
}
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...expand });
return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken };
}
/**
* 验证token
* @param token
* @returns
*/
static async verifyToken(token: string) {
return await UserSecret.verifyToken(token);
}
/**
* 刷新token
* @param refreshToken
* @returns
*/
static async refreshToken(refreshToken: string) {
const token = await oauth.refreshToken(refreshToken);
return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken };
}
static async getOauthUser(token: string) {
return await UserSecret.verifyToken(token);
}
/**
* 清理用户的token需要重新登陆
* @param userid
* @param orgid
* @returns
*/
static async clearUserToken(userid: string, type: 'org' | 'user' = 'user') {
return await oauth.expireUserTokens(userid, type);
}
/**
* 获取用户信息, 并设置tokenUser
* @param token
* @returns
*/
static async getUserByToken(token: string) {
const oauthUser = await UserSecret.verifyToken(token);
if (!oauthUser) {
throw new CustomError('Token is invalid. get UserByToken');
}
const userId = oauthUser?.uid || oauthUser.id;
const user = await User.findByPk(userId);
user.setTokenUser(oauthUser);
return user;
}
/**
* 判断是否在用户列表中, 需要预先设置 tokenUser
* orgs has set curentUser
* @param username
* @param includeMe
* @returns
*/
async hasUser(username: string, includeMe = true) {
const orgs = await this.getOrgs();
const me = this.username;
const allUsers = [...orgs];
if (includeMe) {
allUsers.push(me);
}
return allUsers.includes(username);
}
static async createUser(username: string, password?: string, description?: string) {
const user = await User.findOne({ where: { username } });
if (user) {
throw new CustomError('User already exists');
}
const salt = nanoid(6);
let needChangePassword = !password;
password = password || '123456';
const cPassword = cryptPwd(password, salt);
return await User.create({ username, password: cPassword, description, salt, needChangePassword });
}
static async createOrg(username: string, owner: string, description?: string) {
const user = await User.findOne({ where: { username } });
if (user) {
throw new CustomError('User already exists');
}
const me = await User.findByPk(owner);
if (!me) {
throw new CustomError('Owner not found');
}
if (me.type !== 'user') {
throw new CustomError('Owner type is not user');
}
const org = await Org.create({ username, description, users: [{ uid: owner, role: 'owner' }] });
const newUser = await User.create({ username, password: '', description, type: 'org', owner, orgId: org.id });
// owner add
await redis.del(`user:${me.id}:orgs`);
return newUser;
}
async createPassword(password: string) {
const salt = this.salt;
const cPassword = cryptPwd(password, salt);
this.password = cPassword;
await this.update({ password: cPassword });
return cPassword;
}
checkPassword(password: string) {
const salt = this.salt;
const cPassword = cryptPwd(password, salt);
return this.password === cPassword;
}
/**
* 获取用户信息, 需要先设置 tokenUser 或者设置 uid
* @param uid 如果存在则表示是组织其中uid为真实用户
* @returns
*/
async getInfo(uid?: string) {
const orgs = await this.getOrgs();
const info: Record<string, any> = {
id: this.id,
username: this.username,
nickname: this.nickname,
description: this.description,
needChangePassword: this.needChangePassword,
type: this.type,
avatar: this.avatar,
orgs,
};
const tokenUser = this.tokenUser;
if (uid) {
info.uid = uid;
} else if (tokenUser.uid) {
info.uid = tokenUser.uid;
}
return info;
}
/**
* 获取用户组织
* @returns
*/
async getOrgs() {
let id = this.id;
if (this.type === 'org') {
if (this.tokenUser && this.tokenUser.uid) {
id = this.tokenUser.uid;
} else {
throw new CustomError(400, 'Permission denied');
}
}
const cache = await redis.get(`user:${id}:orgs`);
if (cache) {
return JSON.parse(cache) as string[];
}
const orgs = await Org.findAll({
order: [['updatedAt', 'DESC']],
where: {
users: {
[Op.contains]: [
{
uid: id,
},
],
},
},
});
const orgNames = orgs.map((org) => org.username);
if (orgNames.length > 0) {
await redis.set(`user:${id}:orgs`, JSON.stringify(orgNames), 'EX', 60 * 60); // 1 hour
}
return orgNames;
}
async expireOrgs() {
await redis.del(`user:${this.id}:orgs`);
}
}
export type SyncOpts = {
alter?: boolean;
logging?: any;
force?: boolean;
};
export const UserInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => {
const sequelize = useContextKey<Sequelize>('sequelize');
User.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
// 用户名或者手机号
// 创建后避免修改的字段,当注册用户后,用户名注册则默认不能用手机号
},
nickname: {
type: DataTypes.TEXT,
allowNull: true,
},
password: {
type: DataTypes.STRING,
allowNull: true,
},
email: {
type: DataTypes.STRING,
allowNull: true,
},
avatar: {
type: DataTypes.TEXT,
allowNull: true,
},
salt: {
type: DataTypes.STRING,
allowNull: true,
},
description: {
type: DataTypes.TEXT,
},
type: {
type: DataTypes.STRING,
defaultValue: 'user',
},
owner: {
type: DataTypes.UUID,
},
orgId: {
type: DataTypes.UUID,
},
needChangePassword: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
data: {
type: DataTypes.JSONB,
defaultValue: {},
},
},
{
sequelize: newSequelize || sequelize,
tableName: tableName || 'cf_user', // codeflow user
paranoid: true,
},
);
if (sync) {
await User.sync({ alter: true, logging: true, ...sync })
.then((res) => {
initializeUser();
})
.catch((err) => {
console.error('Sync User error', err);
});
return User;
}
return User;
};
const letter = 'abcdefghijklmnopqrstuvwxyz';
const custom = customAlphabet(letter, 6);
export const initializeUser = async (pwd = custom()) => {
const w = await User.findOne({ where: { username: 'root' }, logging: false });
if (!w) {
const root = await User.createUser('root', pwd, '系统管理员');
const org = await User.createOrg('admin', root.id, '管理员');
console.info(' new Users name', root.username, org.username);
console.info('new Users root password', pwd);
console.info('new Users id', root.id, org.id);
const demo = await createDemoUser();
return {
code: 200,
data: { root, org, pwd: pwd, demo },
};
} else {
return {
code: 500,
message: 'Users has been created',
};
}
};
export const createDemoUser = async (username = 'demo', pwd = custom()) => {
const u = await User.findOne({ where: { username }, logging: false });
if (!u) {
const user = await User.createUser(username, pwd, 'demo');
console.info('new Users name', user.username, pwd);
return {
code: 200,
data: { user, pwd: pwd },
};
} else {
console.info('Users has been created', u.username);
return {
code: 500,
message: 'Users has been created',
};
}
};
// initializeUser();
export class UserServices extends User {
static async loginByPhone(phone: string) {
let user = await User.findOne({ where: { username: phone } });
let isNew = false;
if (!user) {
user = await User.createUser(phone, phone.slice(-6));
isNew = true;
}
const token = await user.createToken(null, 'season');
return { ...token, isNew };
}
static initializeUser = initializeUser;
static createDemoUser = createDemoUser;
}
export const UserModel = useContextKey('UserModel', () => UserServices);

18
src/auth/oauth/auth.ts Normal file
View File

@@ -0,0 +1,18 @@
import { OAuth, RedisTokenStore } from './oauth.ts';
import { useContextKey } from '@kevisual/use-config/context';
import { Redis } from 'ioredis';
export const oauth = useContextKey('oauth', () => {
const redis = useContextKey<Redis>('redis');
const store = new RedisTokenStore(redis);
// redis是promise
if (redis instanceof Promise) {
redis.then((r) => {
store.setRedis(r);
});
} else if (redis) {
store.setRedis(redis);
}
const oauth = new OAuth(store);
return oauth;
});

2
src/auth/oauth/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './oauth.ts';
export * from './salt.ts';

392
src/auth/oauth/oauth.ts Normal file
View File

@@ -0,0 +1,392 @@
/**
* 一个生成和验证token的模块不使用jwt使用redis缓存
* token 分为两种一种是access_token一种是refresh_token
*
* access_token 用于验证用户是否登录过期时间为1小时
* refresh_token 用于刷新access_token过期时间为7天
*
* 生成token时会根据用户信息生成一个access_token和refresh_token并缓存到redis中
* 验证token时会根据token从redis中获取用户信息
* 刷新token时会根据refresh_token生成一个新的access_token和refresh_token并缓存到redis中
*
* 并删除旧的access_token和refresh_token
*
* 生成token的方法使用nanoid生成一个随机字符串
* 验证token的方法使用redis的get方法验证token是否存在
*
* 刷新token的方法使用redis的set方法刷新token
*
* 缓存和获取都可以不使用redis只是用可拓展的接口。store.get和store.set去实现。
*/
import { Redis } from 'ioredis';
import { customAlphabet } from 'nanoid';
export const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
export const randomId16 = customAlphabet(alphabet, 16);
export const randomId24 = customAlphabet(alphabet, 24);
export const randomId32 = customAlphabet(alphabet, 32);
export const randomId64 = customAlphabet(alphabet, 64);
export type OauthUser = {
/**
* 真实用户非org
*/
id: string;
/**
* 组织id非必须存在
*/
orgId?: string;
/**
* 必存在真实用户id
*/
userId: string;
/**
* 当前用户的id如果是org则uid为org的id
*/
uid?: string;
username: string;
type?: 'user' | 'org'; // 用户类型默认是usertoken类型是用于token的扩展
oauthType?: 'user' | 'token'; // 用户类型默认是usertoken类型是用于token的扩展
oauthExpand?: UserExpand;
};
export type UserExpand = {
createTime?: number;
accessToken?: string;
refreshToken?: string;
[key: string]: any;
} & StoreSetOpts;
type StoreSetOpts = {
loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'day'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year'
expire?: number; // 过期时间,单位为秒
hasRefreshToken?: boolean;
[key: string]: any;
};
interface Store<T> {
redis?: Redis;
getObject: (key: string) => Promise<T>;
setObject: (key: string, value: T, opts?: StoreSetOpts) => Promise<void>;
expire: (key: string, ttl?: number) => Promise<void>;
delObject: (value?: T) => Promise<void>;
keys: (key?: string) => Promise<string[]>;
setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<void>;
delKeys: (keys: string[]) => Promise<number>;
}
export class RedisTokenStore implements Store<OauthUser> {
redis: Redis;
private prefix: string = 'oauth:';
constructor(redis?: Redis, prefix?: string) {
this.redis = redis;
this.prefix = prefix || this.prefix;
}
async setRedis(redis: Redis) {
this.redis = redis;
}
async set(key: string, value: string, ttl?: number) {
await this.redis.set(this.prefix + key, value, 'EX', ttl);
}
async get(key: string) {
return await this.redis.get(this.prefix + key);
}
async expire(key: string, ttl?: number) {
await this.redis.expire(this.prefix + key, ttl);
}
async keys(key?: string) {
return await this.redis.keys(this.prefix + key);
}
async getObject(key: string) {
try {
const value = await this.get(key);
if (!value) {
return null;
}
return JSON.parse(value);
} catch (error) {
console.log('get key parse error', error);
return null;
}
}
async del(key: string) {
const number = await this.redis.del(this.prefix + key);
return number;
}
async setObject(key: string, value: OauthUser, opts?: StoreSetOpts) {
await this.set(key, JSON.stringify(value), opts?.expire);
}
async delObject(value?: OauthUser) {
const refreshToken = value?.oauthExpand?.refreshToken;
const accessToken = value?.oauthExpand?.accessToken;
// 清理userPerfix
let userPrefix = 'user:' + value?.id;
if (value?.orgId) {
userPrefix = 'org:' + value?.orgId + ':user:' + value?.id;
}
if (refreshToken) {
await this.del(refreshToken);
await this.del(userPrefix + ':refreshToken:' + refreshToken);
}
if (accessToken) {
await this.del(accessToken);
await this.del(userPrefix + ':token:' + accessToken);
}
}
async setToken(data: { accessToken: string; refreshToken: string; value?: OauthUser }, opts?: StoreSetOpts) {
const { accessToken, refreshToken, value } = data;
let userPrefix = 'user:' + value?.id;
if (value?.orgId) {
userPrefix = 'org:' + value?.orgId + ':user:' + value?.id;
}
// 计算过期时间根据opts.expire 和 opts.loginType
// 如果expire存在则使用expire否则使用opts.loginType 进行计算;
let expire = opts?.expire;
if (!expire) {
switch (opts.loginType) {
case 'day':
expire = 24 * 60 * 60;
break;
case 'week':
expire = 7 * 24 * 60 * 60;
break;
case 'month':
expire = 30 * 24 * 60 * 60;
break;
case 'season':
expire = 90 * 24 * 60 * 60;
break;
default:
expire = 7 * 24 * 60 * 60; // 默认过期时间为7天
}
} else {
expire = Math.min(expire, 60 * 60 * 24 * 30, 60 * 60 * 24 * 90); // 默认的过期时间最大为90天
}
await this.set(accessToken, JSON.stringify(value), expire);
await this.set(userPrefix + ':token:' + accessToken, accessToken, expire);
if (refreshToken) {
let refreshTokenExpire = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
// 小于7天, 则设置为7天
if (refreshTokenExpire < 60 * 60 * 24 * 7) {
refreshTokenExpire = 60 * 60 * 24 * 7;
}
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpire);
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpire);
}
}
async delKeys(keys: string[]) {
const prefix = this.prefix;
const number = await this.redis.del(keys.map((key) => prefix + key));
return number;
}
}
export class OAuth<T extends OauthUser> {
private store: Store<T>;
constructor(store: Store<T>) {
this.store = store;
}
generateSecretKey(sk = true) {
if (sk) {
return 'sk_' + randomId64();
}
return 'st_' + randomId32();
}
/**
* 生成token
* @param user
* @param user.id 访问者id
* @param user.uid 如果是org这个是真实用户idid是orgId
* @param user.userId 真实用户id
* @param user.orgId 组织id可选
* @param user.username
* @param user.type
* @returns
*/
async generateToken(
user: T,
expandOpts?: StoreSetOpts,
): Promise<{
accessToken: string;
refreshToken?: string;
}> {
// 拥有refreshToken 为 true 时accessToken 为 st_ 开头refreshToken 为 rk_开头
// 意思是secretToken 和 secretKey的缩写
const accessToken = expandOpts?.hasRefreshToken ? 'st_' + randomId32() : 'sk_' + randomId64();
const refreshToken = expandOpts?.hasRefreshToken ? 'rk_' + randomId64() : null;
// 初始化 appExpand
user.oauthExpand = user.oauthExpand || {};
if (expandOpts) {
user.oauthExpand = {
...user.oauthExpand,
...expandOpts,
accessToken,
createTime: new Date().getTime(), //
};
if (expandOpts?.hasRefreshToken) {
user.oauthExpand.refreshToken = refreshToken;
}
}
await this.store.setToken({ accessToken, refreshToken, value: user }, expandOpts);
return { accessToken, refreshToken };
}
async saveSecretKey(oauthUser: T, secretKey: string, opts?: StoreSetOpts) {
// 生成一个secretKey
// 设置到store中
oauthUser.oauthExpand = {
...oauthUser.oauthExpand,
accessToken: secretKey,
description: 'secretKey',
createTime: new Date().getTime(), // 创建时间
};
await this.store.setToken(
{ accessToken: secretKey, refreshToken: '', value: oauthUser },
{
...opts,
hasRefreshToken: false,
},
);
return secretKey;
}
getOauthUser({ id, uid, username, type }: Partial<T>): OauthUser {
const oauthUser: OauthUser = {
id,
username,
uid,
userId: uid || id, // 必存在真实用户id
type: type as 'user' | 'org',
};
if (uid) {
oauthUser.orgId = id;
}
return oauthUser;
}
/**
* 验证token如果token不存在返回null
* @param token
* @returns
*/
async verifyToken(token: string) {
const res = await this.store.getObject(token);
return res;
}
/**
* 验证token是否是accessToken, sk 开头的为secretKey没有refreshToken
* @param token
* @returns
*/
isSecretKey(token: string) {
if (!token) {
return false;
}
// 如果是sk_开头则是secretKey
if (token.startsWith('sk_')) {
return true;
}
return false;
}
/**
* 刷新token
* @param refreshToken
* @returns
*/
async refreshToken(refreshToken: string) {
const user = await this.store.getObject(refreshToken);
if (!user) {
// 过期
throw new Error('Refresh token not found');
}
// 删除旧的token
await this.store.delObject({ ...user });
const token = await this.generateToken(
{ ...user },
{
...user.oauthExpand,
hasRefreshToken: true,
},
);
console.log('resetToken token', await this.store.keys());
return token;
}
/**
* 刷新token的过期时间
* expand 为扩展参数可以扩展到user.oauthExpand中
* @param token
* @returns
*/
async resetToken(accessToken: string, expand?: Record<string, any>) {
const user = await this.store.getObject(accessToken);
if (!user) {
// 过期
throw new Error('token not found');
}
user.oauthExpand = user.oauthExpand || {};
const refreshToken = user.oauthExpand.refreshToken;
if (refreshToken) {
await this.store.delObject(user);
}
user.oauthExpand = {
...user.oauthExpand,
...expand,
};
const token = await this.generateToken(
{ ...user },
{
...user.oauthExpand,
hasRefreshToken: true,
},
);
return token;
}
/**
* 过期token
* @param token
*/
async delToken(token: string) {
const user = await this.store.getObject(token);
if (!user) {
// 过期
throw new Error('token not found');
}
this.store.delObject(user);
}
/**
* 获取某一个用户的所有token
* @param userId
* @returns
*/
async getUserTokens(userId: string, orgId?: string) {
const userPrefix = orgId ? `org:${orgId}:user:${userId}` : `user:${userId}`;
const tokens = await this.store.keys(`${userPrefix}:token:*`);
return tokens;
}
/**
* 过期某一个用户的所有token
* @param userId
* @param orgId
*/
async expireUserTokens(userId: string, type: 'user' | 'org' = 'user') {
const userPrefix = type === 'org' ? `org:${userId}:user:*:` : `user:${userId}`;
const tokensKeys = await this.store.keys(`${userPrefix}:token:*`);
for (const tokenKey of tokensKeys) {
try {
const token = await this.store.redis.get(tokenKey);
const user = await this.store.getObject(token);
this.store.delObject(user);
} catch (error) {
console.error('expireUserTokens error', userId, type, error);
}
}
}
/**
* 过期所有用户的token 然后重启服务
*/
async expireAllTokens() {
const tokens = await this.store.keys('*');
await this.store.delKeys(tokens);
}
}

32
src/auth/oauth/salt.ts Normal file
View File

@@ -0,0 +1,32 @@
import MD5 from 'crypto-js/md5.js';
/**
* 生成随机盐
* @returns
*/
export const getRandomSalt = () => {
return Math.random().toString().slice(2, 7);
};
/**
* 加密密码
* @param password
* @param salt
* @returns
*/
export const cryptPwd = (password: string, salt = '') => {
const saltPassword = password + ':' + salt;
const md5 = MD5(saltPassword);
return md5.toString();
};
/**
* Check password
* @param password
* @param salt
* @param md5
* @returns
*/
export const checkPwd = (password: string, salt: string, md5: string) => {
return cryptPwd(password, salt) === md5;
};