릴리스: v1.4.45 이메일 인증 및 비밀번호 재설정 추가
This commit is contained in:
@@ -59,6 +59,7 @@ function mapUserRow(row) {
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
nickname: row.nickname || '',
|
||||
emailVerified: row.email_verified == null ? true : !!row.email_verified,
|
||||
isAdmin: !!row.is_admin,
|
||||
avatarSrc: row.avatar_src || '',
|
||||
createdAt: Number(row.created_at),
|
||||
@@ -275,12 +276,47 @@ async function ensureSchema() {
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
nickname VARCHAR(80) NOT NULL DEFAULT '',
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
email_verified TINYINT(1) NOT NULL DEFAULT 1,
|
||||
is_admin TINYINT(1) NOT NULL DEFAULT 0,
|
||||
avatar_src VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at BIGINT NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
const userEmailVerifiedColumns = await query("SHOW COLUMNS FROM users LIKE 'email_verified'")
|
||||
if (!userEmailVerifiedColumns.length) {
|
||||
await query('ALTER TABLE users ADD COLUMN email_verified TINYINT(1) NOT NULL DEFAULT 1 AFTER password_hash')
|
||||
await query('UPDATE users SET email_verified = 1 WHERE email_verified IS NULL')
|
||||
}
|
||||
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS email_verification_tokens (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
user_id VARCHAR(64) NOT NULL,
|
||||
token_hash CHAR(64) NOT NULL UNIQUE,
|
||||
expires_at BIGINT NOT NULL,
|
||||
consumed_at BIGINT NOT NULL DEFAULT 0,
|
||||
created_at BIGINT NOT NULL,
|
||||
INDEX idx_email_verification_user (user_id, consumed_at, expires_at),
|
||||
INDEX idx_email_verification_expires (expires_at),
|
||||
CONSTRAINT fk_email_verification_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
user_id VARCHAR(64) NOT NULL,
|
||||
token_hash CHAR(64) NOT NULL UNIQUE,
|
||||
expires_at BIGINT NOT NULL,
|
||||
consumed_at BIGINT NOT NULL DEFAULT 0,
|
||||
created_at BIGINT NOT NULL,
|
||||
INDEX idx_password_reset_user (user_id, consumed_at, expires_at),
|
||||
INDEX idx_password_reset_expires (expires_at),
|
||||
CONSTRAINT fk_password_reset_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`)
|
||||
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS topics (
|
||||
id VARCHAR(120) PRIMARY KEY,
|
||||
@@ -567,7 +603,7 @@ async function countUsers() {
|
||||
|
||||
async function findUserByEmail(email) {
|
||||
const rows = await query(
|
||||
'SELECT id, email, nickname, password_hash, is_admin, avatar_src, created_at FROM users WHERE email = ? LIMIT 1',
|
||||
'SELECT id, email, nickname, password_hash, email_verified, is_admin, avatar_src, created_at FROM users WHERE email = ? LIMIT 1',
|
||||
[email]
|
||||
)
|
||||
const row = rows[0]
|
||||
@@ -581,7 +617,7 @@ async function findUserByNickname(nickname, excludeUserId = '') {
|
||||
const rows = excludeUserId
|
||||
? await query(
|
||||
`
|
||||
SELECT id, email, nickname, password_hash, is_admin, avatar_src, created_at
|
||||
SELECT id, email, nickname, password_hash, email_verified, is_admin, avatar_src, created_at
|
||||
FROM users
|
||||
WHERE TRIM(LOWER(nickname)) = TRIM(LOWER(?)) AND id <> ?
|
||||
LIMIT 1
|
||||
@@ -590,7 +626,7 @@ async function findUserByNickname(nickname, excludeUserId = '') {
|
||||
)
|
||||
: await query(
|
||||
`
|
||||
SELECT id, email, nickname, password_hash, is_admin, avatar_src, created_at
|
||||
SELECT id, email, nickname, password_hash, email_verified, is_admin, avatar_src, created_at
|
||||
FROM users
|
||||
WHERE TRIM(LOWER(nickname)) = TRIM(LOWER(?))
|
||||
LIMIT 1
|
||||
@@ -604,24 +640,138 @@ async function findUserByNickname(nickname, excludeUserId = '') {
|
||||
|
||||
async function findUserById(id) {
|
||||
const rows = await query(
|
||||
'SELECT id, email, nickname, is_admin, avatar_src, created_at FROM users WHERE id = ? LIMIT 1',
|
||||
'SELECT id, email, nickname, email_verified, is_admin, avatar_src, created_at FROM users WHERE id = ? LIMIT 1',
|
||||
[id]
|
||||
)
|
||||
return mapUserRow(rows[0])
|
||||
}
|
||||
|
||||
async function createUser({ id, email, nickname, passwordHash, isAdmin }) {
|
||||
async function createUser({ id, email, nickname, passwordHash, emailVerified = true, isAdmin }) {
|
||||
const createdAt = now()
|
||||
await query(
|
||||
`
|
||||
INSERT INTO users (id, email, nickname, password_hash, is_admin, avatar_src, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO users (id, email, nickname, password_hash, email_verified, is_admin, avatar_src, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[id, email, nickname || '', passwordHash, isAdmin ? 1 : 0, '', createdAt]
|
||||
[id, email, nickname || '', passwordHash, emailVerified ? 1 : 0, isAdmin ? 1 : 0, '', createdAt]
|
||||
)
|
||||
return findUserById(id)
|
||||
}
|
||||
|
||||
async function updateUserPassword({ id, passwordHash }) {
|
||||
await query('UPDATE users SET password_hash = ? WHERE id = ?', [passwordHash, id])
|
||||
return findUserById(id)
|
||||
}
|
||||
|
||||
async function verifyUserEmail(userId) {
|
||||
await query('UPDATE users SET email_verified = 1 WHERE id = ?', [userId])
|
||||
return findUserById(userId)
|
||||
}
|
||||
|
||||
async function createEmailVerificationToken({ id, userId, tokenHash, expiresAt }) {
|
||||
const createdAt = now()
|
||||
await query('UPDATE email_verification_tokens SET consumed_at = ? WHERE user_id = ? AND consumed_at = 0', [createdAt, userId])
|
||||
await query(
|
||||
`
|
||||
INSERT INTO email_verification_tokens (id, user_id, token_hash, expires_at, consumed_at, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[id, userId, tokenHash, expiresAt, 0, createdAt]
|
||||
)
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
tokenHash,
|
||||
expiresAt,
|
||||
consumedAt: 0,
|
||||
createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
async function findEmailVerificationTokenByHash(tokenHash) {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
token_hash,
|
||||
expires_at,
|
||||
consumed_at,
|
||||
created_at
|
||||
FROM email_verification_tokens
|
||||
WHERE token_hash = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
[tokenHash]
|
||||
)
|
||||
const row = rows[0]
|
||||
if (!row) return null
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
tokenHash: row.token_hash,
|
||||
expiresAt: Number(row.expires_at || 0),
|
||||
consumedAt: Number(row.consumed_at || 0),
|
||||
createdAt: Number(row.created_at || 0),
|
||||
}
|
||||
}
|
||||
|
||||
async function consumeEmailVerificationToken(tokenId) {
|
||||
await query('UPDATE email_verification_tokens SET consumed_at = ? WHERE id = ? AND consumed_at = 0', [now(), tokenId])
|
||||
}
|
||||
|
||||
async function createPasswordResetToken({ id, userId, tokenHash, expiresAt }) {
|
||||
const createdAt = now()
|
||||
await query('UPDATE password_reset_tokens SET consumed_at = ? WHERE user_id = ? AND consumed_at = 0', [createdAt, userId])
|
||||
await query(
|
||||
`
|
||||
INSERT INTO password_reset_tokens (id, user_id, token_hash, expires_at, consumed_at, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[id, userId, tokenHash, expiresAt, 0, createdAt]
|
||||
)
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
tokenHash,
|
||||
expiresAt,
|
||||
consumedAt: 0,
|
||||
createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
async function findPasswordResetTokenByHash(tokenHash) {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
token_hash,
|
||||
expires_at,
|
||||
consumed_at,
|
||||
created_at
|
||||
FROM password_reset_tokens
|
||||
WHERE token_hash = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
[tokenHash]
|
||||
)
|
||||
const row = rows[0]
|
||||
if (!row) return null
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
tokenHash: row.token_hash,
|
||||
expiresAt: Number(row.expires_at || 0),
|
||||
consumedAt: Number(row.consumed_at || 0),
|
||||
createdAt: Number(row.created_at || 0),
|
||||
}
|
||||
}
|
||||
|
||||
async function consumePasswordResetToken(tokenId) {
|
||||
await query('UPDATE password_reset_tokens SET consumed_at = ? WHERE id = ? AND consumed_at = 0', [now(), tokenId])
|
||||
}
|
||||
|
||||
async function updateUserProfile({ id, nickname, avatarSrc }) {
|
||||
if (typeof avatarSrc === 'string') {
|
||||
await query('UPDATE users SET nickname = ?, avatar_src = ? WHERE id = ?', [nickname || '', avatarSrc, id])
|
||||
@@ -633,7 +783,7 @@ async function updateUserProfile({ id, nickname, avatarSrc }) {
|
||||
|
||||
async function findPrimaryAdminUser() {
|
||||
const rows = await query(
|
||||
'SELECT id, email, nickname, is_admin, avatar_src, created_at FROM users WHERE is_admin = 1 ORDER BY created_at ASC, email ASC LIMIT 1'
|
||||
'SELECT id, email, nickname, email_verified, is_admin, avatar_src, created_at FROM users WHERE is_admin = 1 ORDER BY created_at ASC, email ASC LIMIT 1'
|
||||
)
|
||||
return mapUserRow(rows[0])
|
||||
}
|
||||
@@ -668,6 +818,7 @@ async function listUsers({ queryText = '', sort = 'recent', direction = 'desc' }
|
||||
u.id,
|
||||
u.email,
|
||||
u.nickname,
|
||||
u.email_verified,
|
||||
u.is_admin,
|
||||
u.avatar_src,
|
||||
u.created_at,
|
||||
@@ -679,7 +830,7 @@ async function listUsers({ queryText = '', sort = 'recent', direction = 'desc' }
|
||||
FROM users u
|
||||
LEFT JOIN tierlists t ON t.author_id = u.id
|
||||
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
||||
GROUP BY u.id, u.email, u.nickname, u.is_admin, u.avatar_src, u.created_at
|
||||
GROUP BY u.id, u.email, u.nickname, u.email_verified, u.is_admin, u.avatar_src, u.created_at
|
||||
ORDER BY ${orderBy}
|
||||
`,
|
||||
params
|
||||
@@ -2501,6 +2652,14 @@ module.exports = {
|
||||
findUserByNickname,
|
||||
findUserById,
|
||||
createUser,
|
||||
updateUserPassword,
|
||||
verifyUserEmail,
|
||||
createEmailVerificationToken,
|
||||
findEmailVerificationTokenByHash,
|
||||
consumeEmailVerificationToken,
|
||||
createPasswordResetToken,
|
||||
findPasswordResetTokenByHash,
|
||||
consumePasswordResetToken,
|
||||
updateUserProfile,
|
||||
findPrimaryAdminUser,
|
||||
listUsers,
|
||||
|
||||
Reference in New Issue
Block a user