feat(server): 新增云端缓存与同步服务端骨架
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
146
server/src/auth.js
Normal file
146
server/src/auth.js
Normal file
@@ -0,0 +1,146 @@
|
||||
const crypto = require('crypto');
|
||||
const db = require('./db');
|
||||
|
||||
const TOKEN_TTL_DAYS = 30;
|
||||
|
||||
function parseJsonMaybe(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sha256(value) {
|
||||
return crypto.createHash('sha256').update(String(value || '')).digest('hex');
|
||||
}
|
||||
|
||||
function randomToken(size) {
|
||||
return crypto.randomBytes(size).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
function normalizeEmail(email) {
|
||||
return String(email || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function hashPassword(password) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var salt = crypto.randomBytes(16).toString('base64');
|
||||
crypto.scrypt(String(password), salt, 64, function(error, derivedKey) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve('scrypt$' + salt + '$' + derivedKey.toString('base64'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyPassword(password, storedHash) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var parts = String(storedHash || '').split('$');
|
||||
if (parts.length !== 3 || parts[0] !== 'scrypt') {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
var salt = parts[1];
|
||||
var expected = Buffer.from(parts[2], 'base64');
|
||||
crypto.scrypt(String(password), salt, expected.length, function(error, derivedKey) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(crypto.timingSafeEqual(expected, derivedKey));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createAuthToken(userId, deviceFingerprint) {
|
||||
var rawToken = randomToken(32);
|
||||
var expiresAt = new Date(Date.now() + TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);
|
||||
await db.execute(
|
||||
'INSERT INTO auth_tokens (user_id, token_hash, device_fingerprint, expires_at, last_seen_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)',
|
||||
[userId, sha256(rawToken), deviceFingerprint || null, expiresAt]
|
||||
);
|
||||
return rawToken;
|
||||
}
|
||||
|
||||
async function touchAuthToken(tokenHash) {
|
||||
await db.execute('UPDATE auth_tokens SET last_seen_at = CURRENT_TIMESTAMP WHERE token_hash = ?', [tokenHash]);
|
||||
}
|
||||
|
||||
async function revokeAuthToken(tokenHash) {
|
||||
await db.execute('UPDATE auth_tokens SET revoked_at = CURRENT_TIMESTAMP WHERE token_hash = ? AND revoked_at IS NULL', [tokenHash]);
|
||||
}
|
||||
|
||||
async function getAuthContextFromToken(rawToken) {
|
||||
if (!rawToken) {
|
||||
return null;
|
||||
}
|
||||
var tokenHash = sha256(rawToken);
|
||||
var rows = await db.query(
|
||||
'SELECT t.id AS token_id, t.user_id, t.token_hash, t.device_fingerprint, t.expires_at, t.revoked_at, u.email, u.status, k.wrapped_dek, k.kdf_salt, k.kdf_params, k.key_version FROM auth_tokens t INNER JOIN users u ON u.id = t.user_id LEFT JOIN user_keyrings k ON k.user_id = u.id WHERE t.token_hash = ? LIMIT 1',
|
||||
[tokenHash]
|
||||
);
|
||||
if (!rows.length) {
|
||||
return null;
|
||||
}
|
||||
var row = rows[0];
|
||||
if (row.revoked_at) {
|
||||
return null;
|
||||
}
|
||||
if (new Date(row.expires_at).getTime() <= Date.now()) {
|
||||
return null;
|
||||
}
|
||||
if (Number(row.status || 0) !== 1) {
|
||||
return null;
|
||||
}
|
||||
await touchAuthToken(tokenHash);
|
||||
return {
|
||||
tokenHash: tokenHash,
|
||||
tokenId: row.token_id,
|
||||
user: {
|
||||
id: row.user_id,
|
||||
email: row.email
|
||||
},
|
||||
keyring: row.wrapped_dek ? {
|
||||
wrappedDek: row.wrapped_dek,
|
||||
kdfSalt: row.kdf_salt,
|
||||
kdfParams: parseJsonMaybe(row.kdf_params),
|
||||
keyVersion: Number(row.key_version || 1)
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
async function requireAuth(request, reply) {
|
||||
var header = request.headers.authorization || '';
|
||||
var match = String(header).match(/^Bearer\s+(.+)$/i);
|
||||
if (!match) {
|
||||
reply.code(401);
|
||||
throw new Error('未登录');
|
||||
}
|
||||
var authContext = await getAuthContextFromToken(match[1]);
|
||||
if (!authContext) {
|
||||
reply.code(401);
|
||||
throw new Error('登录已失效');
|
||||
}
|
||||
request.authContext = authContext;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createAuthToken,
|
||||
getAuthContextFromToken,
|
||||
hashPassword,
|
||||
normalizeEmail,
|
||||
parseJsonMaybe,
|
||||
requireAuth,
|
||||
revokeAuthToken,
|
||||
sha256,
|
||||
verifyPassword
|
||||
};
|
||||
Reference in New Issue
Block a user