v0.1.0
This commit is contained in:
206
models/OAuthClient.js
Normal file
206
models/OAuthClient.js
Normal file
@ -0,0 +1,206 @@
|
||||
const pool = require('../config/database');
|
||||
const crypto = require('crypto');
|
||||
|
||||
class OAuthClient {
|
||||
// 创建OAuth客户端表
|
||||
static async createTable() {
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS oauth_clients (
|
||||
id SERIAL PRIMARY KEY,
|
||||
client_id VARCHAR(100) UNIQUE NOT NULL,
|
||||
client_secret VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
redirect_uris TEXT[] NOT NULL,
|
||||
scopes TEXT[] DEFAULT ARRAY['read', 'write'],
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
try {
|
||||
await pool.query(query);
|
||||
console.log('OAuth客户端表创建成功');
|
||||
} catch (error) {
|
||||
console.error('创建OAuth客户端表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建授权码表
|
||||
static async createAuthCodeTable() {
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS oauth_auth_codes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code VARCHAR(100) UNIQUE NOT NULL,
|
||||
client_id VARCHAR(100) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
redirect_uri VARCHAR(255) NOT NULL,
|
||||
scopes TEXT[],
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
try {
|
||||
await pool.query(query);
|
||||
console.log('OAuth授权码表创建成功');
|
||||
} catch (error) {
|
||||
console.error('创建OAuth授权码表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建访问令牌表
|
||||
static async createAccessTokenTable() {
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS oauth_access_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
token VARCHAR(255) UNIQUE NOT NULL,
|
||||
client_id VARCHAR(100) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
scopes TEXT[],
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
try {
|
||||
await pool.query(query);
|
||||
console.log('OAuth访问令牌表创建成功');
|
||||
} catch (error) {
|
||||
console.error('创建OAuth访问令牌表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建刷新令牌表
|
||||
static async createRefreshTokenTable() {
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
token VARCHAR(255) UNIQUE NOT NULL,
|
||||
access_token_id INTEGER REFERENCES oauth_access_tokens(id) ON DELETE CASCADE,
|
||||
client_id VARCHAR(100) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
is_revoked BOOLEAN DEFAULT FALSE,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
try {
|
||||
await pool.query(query);
|
||||
console.log('OAuth刷新令牌表创建成功');
|
||||
} catch (error) {
|
||||
console.error('创建OAuth刷新令牌表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成客户端ID和密钥
|
||||
static generateClientCredentials() {
|
||||
const clientId = crypto.randomBytes(32).toString('hex');
|
||||
const clientSecret = crypto.randomBytes(64).toString('hex');
|
||||
return { clientId, clientSecret };
|
||||
}
|
||||
|
||||
// 创建新的OAuth客户端
|
||||
static async create(clientData) {
|
||||
const { name, description, redirectUris, scopes, userId } = clientData;
|
||||
const { clientId, clientSecret } = this.generateClientCredentials();
|
||||
|
||||
const query = `
|
||||
INSERT INTO oauth_clients (client_id, client_secret, name, description, redirect_uris, scopes, user_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, client_id, client_secret, name, description, redirect_uris, scopes, created_at
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [
|
||||
clientId,
|
||||
clientSecret,
|
||||
name,
|
||||
description,
|
||||
redirectUris,
|
||||
scopes || ['read', 'write'],
|
||||
userId
|
||||
]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('创建OAuth客户端失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置客户端密钥
|
||||
static async resetClientSecret(clientId) {
|
||||
const newSecret = crypto.randomBytes(64).toString('hex');
|
||||
const query = 'UPDATE oauth_clients SET client_secret = $1 WHERE client_id = $2 AND is_active = true';
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [newSecret, clientId]);
|
||||
if (result.rowCount === 0) {
|
||||
throw new Error('客户端不存在或已禁用');
|
||||
}
|
||||
return newSecret;
|
||||
} catch (error) {
|
||||
console.error('重置客户端密钥失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据客户端ID查找客户端
|
||||
static async findByClientId(clientId) {
|
||||
const query = 'SELECT * FROM oauth_clients WHERE client_id = $1 AND is_active = true';
|
||||
try {
|
||||
const result = await pool.query(query, [clientId]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('查找OAuth客户端失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证客户端密钥
|
||||
static async validateClient(clientId, clientSecret) {
|
||||
const client = await this.findByClientId(clientId);
|
||||
if (!client) return false;
|
||||
return client.client_secret === clientSecret;
|
||||
}
|
||||
|
||||
// 验证重定向URI
|
||||
static async validateRedirectUri(clientId, redirectUri) {
|
||||
const client = await this.findByClientId(clientId);
|
||||
if (!client) return false;
|
||||
return client.redirect_uris.includes(redirectUri);
|
||||
}
|
||||
|
||||
// 获取用户的所有客户端
|
||||
static async findByUserId(userId) {
|
||||
const query = 'SELECT id, client_id, name, description, redirect_uris, scopes, created_at FROM oauth_clients WHERE user_id = $1 AND is_active = true';
|
||||
try {
|
||||
const result = await pool.query(query, [userId]);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
console.error('查找用户客户端失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除客户端
|
||||
static async delete(clientId, userId) {
|
||||
const query = 'UPDATE oauth_clients SET is_active = false WHERE client_id = $1 AND user_id = $2';
|
||||
try {
|
||||
const result = await pool.query(query, [clientId, userId]);
|
||||
return result.rowCount > 0;
|
||||
} catch (error) {
|
||||
console.error('删除OAuth客户端失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OAuthClient;
|
207
models/OAuthToken.js
Normal file
207
models/OAuthToken.js
Normal file
@ -0,0 +1,207 @@
|
||||
const pool = require('../config/database');
|
||||
const crypto = require('crypto');
|
||||
|
||||
class OAuthToken {
|
||||
// 生成授权码
|
||||
static generateAuthCode() {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
// 生成访问令牌
|
||||
static generateAccessToken() {
|
||||
return crypto.randomBytes(64).toString('hex');
|
||||
}
|
||||
|
||||
// 生成刷新令牌
|
||||
static generateRefreshToken() {
|
||||
return crypto.randomBytes(64).toString('hex');
|
||||
}
|
||||
|
||||
// 创建授权码
|
||||
static async createAuthCode(authCodeData) {
|
||||
const { code, clientId, userId, redirectUri, scopes } = authCodeData;
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10分钟过期
|
||||
|
||||
const query = `
|
||||
INSERT INTO oauth_auth_codes (code, client_id, user_id, redirect_uri, scopes, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [code, clientId, userId, redirectUri, scopes, expiresAt]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('创建授权码失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证授权码
|
||||
static async validateAuthCode(code, clientId, redirectUri) {
|
||||
const query = `
|
||||
SELECT * FROM oauth_auth_codes
|
||||
WHERE code = $1 AND client_id = $2 AND redirect_uri = $3 AND expires_at > NOW()
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [code, clientId, redirectUri]);
|
||||
// console.log(result.rows[0]);
|
||||
// console.log(code, clientId, redirectUri);
|
||||
// // console.log();
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('验证授权码失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除授权码
|
||||
static async deleteAuthCode(code) {
|
||||
const query = 'DELETE FROM oauth_auth_codes WHERE code = $1';
|
||||
try {
|
||||
await pool.query(query, [code]);
|
||||
} catch (error) {
|
||||
console.error('删除授权码失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建访问令牌
|
||||
static async createAccessToken(accessTokenData) {
|
||||
const { token, clientId, userId, scopes } = accessTokenData;
|
||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1小时过期
|
||||
|
||||
const query = `
|
||||
INSERT INTO oauth_access_tokens (token, client_id, user_id, scopes, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [token, clientId, userId, scopes, expiresAt]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('创建访问令牌失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建刷新令牌
|
||||
static async createRefreshToken(refreshTokenData) {
|
||||
const { token, accessTokenId, clientId, userId } = refreshTokenData;
|
||||
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30天过期
|
||||
|
||||
const query = `
|
||||
INSERT INTO oauth_refresh_tokens (token, access_token_id, client_id, user_id, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [token, accessTokenId, clientId, userId, expiresAt]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('创建刷新令牌失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证访问令牌
|
||||
static async validateAccessToken(token) {
|
||||
const query = `
|
||||
SELECT oat.*, u.username, u.email
|
||||
FROM oauth_access_tokens oat
|
||||
JOIN users u ON oat.user_id = u.id
|
||||
WHERE oat.token = $1 AND oat.expires_at > NOW()
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [token]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('验证访问令牌失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销刷新令牌
|
||||
static async revokeRefreshToken(token) {
|
||||
const query = 'UPDATE oauth_refresh_tokens SET is_revoked = true WHERE token = $1';
|
||||
try {
|
||||
await pool.query(query, [token]);
|
||||
} catch (error) {
|
||||
console.error('撤销刷新令牌失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证刷新令牌
|
||||
static async validateRefreshToken(token, clientId) {
|
||||
const query = `
|
||||
SELECT rt.*, at.scopes
|
||||
FROM oauth_refresh_tokens rt
|
||||
JOIN oauth_access_tokens at ON rt.access_token_id = at.id
|
||||
WHERE rt.token = $1 AND rt.client_id = $2 AND rt.is_revoked = false AND rt.expires_at > NOW()
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [token, clientId]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('验证刷新令牌失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销访问令牌
|
||||
static async revokeAccessToken(token) {
|
||||
const query = 'DELETE FROM oauth_access_tokens WHERE token = $1';
|
||||
try {
|
||||
const result = await pool.query(query, [token]);
|
||||
return result.rowCount > 0;
|
||||
} catch (error) {
|
||||
console.error('撤销访问令牌失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期令牌(改进版)
|
||||
static async cleanupExpiredTokens() {
|
||||
try {
|
||||
// 清理过期的访问令牌
|
||||
await pool.query('DELETE FROM oauth_access_tokens WHERE expires_at < NOW()');
|
||||
|
||||
// 清理过期的刷新令牌
|
||||
await pool.query('DELETE FROM oauth_refresh_tokens WHERE expires_at < NOW()');
|
||||
|
||||
// 清理过期的授权码
|
||||
await pool.query('DELETE FROM oauth_auth_codes WHERE expires_at < NOW()');
|
||||
|
||||
console.log('已清理过期令牌');
|
||||
} catch (error) {
|
||||
console.error('清理过期令牌失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户的活跃令牌
|
||||
static async getActiveTokensByUserId(userId) {
|
||||
const query = `
|
||||
SELECT oat.token, oat.client_id, oat.scopes, oat.expires_at, oc.name as client_name
|
||||
FROM oauth_access_tokens oat
|
||||
JOIN oauth_clients oc ON oat.client_id = oc.client_id
|
||||
WHERE oat.user_id = $1 AND oat.expires_at > NOW()
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [userId]);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
console.error('获取用户活跃令牌失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OAuthToken;
|
92
models/User.js
Normal file
92
models/User.js
Normal file
@ -0,0 +1,92 @@
|
||||
const pool = require('../config/database');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
class User {
|
||||
// 创建用户表
|
||||
static async createTable() {
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
try {
|
||||
await pool.query(query);
|
||||
console.log('用户表创建成功');
|
||||
} catch (error) {
|
||||
console.error('创建用户表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据用户名查找用户
|
||||
static async findByUsername(username) {
|
||||
const query = 'SELECT * FROM users WHERE username = $1';
|
||||
try {
|
||||
const result = await pool.query(query, [username]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('查找用户失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据邮箱查找用户
|
||||
static async findByEmail(email) {
|
||||
const query = 'SELECT * FROM users WHERE email = $1';
|
||||
try {
|
||||
const result = await pool.query(query, [email]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('查找用户失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
static async create(userData) {
|
||||
const { username, email, password } = userData;
|
||||
|
||||
// 加密密码
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
const query = `
|
||||
INSERT INTO users (username, email, password)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, username, email, created_at
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [username, email, hashedPassword]);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('创建用户失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
static async verifyPassword(password, hashedPassword) {
|
||||
return await bcrypt.compare(password, hashedPassword);
|
||||
}
|
||||
|
||||
// 获取所有用户(仅用于测试)
|
||||
static async findAll() {
|
||||
const query = 'SELECT id, username, email, created_at FROM users';
|
||||
try {
|
||||
const result = await pool.query(query);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User;
|
Reference in New Issue
Block a user