Files
uniare.sori.studio/api/src/db.js
zenn 5a56ddcede v4.0.10: 관리자 계정 변경 안전장치와 접속시간 지표 추가
역할 변경은 명시적으로 버튼을 눌렀을 때만 가능하게 제한하고, 마지막 접속시간과 비밀번호 분실 안내를 추가해 운영 관리성을 높인다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 18:24:45 +09:00

118 lines
4.2 KiB
JavaScript

import Database from 'better-sqlite3';
import bcrypt from 'bcryptjs';
import fs from 'fs';
import path from 'path';
/**
* SQLite DB 연결 및 스키마 초기화
* @param {string} dbPath
* @returns {import('better-sqlite3').Database}
*/
export function initDb(dbPath) {
const dir = path.dirname(dbPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
display_name TEXT,
avatar_url TEXT,
role TEXT NOT NULL DEFAULT 'user',
is_active INTEGER NOT NULL DEFAULT 1,
last_login_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS decks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
share_code TEXT NOT NULL UNIQUE,
share_type TEXT NOT NULL DEFAULT 'user',
content_hash TEXT,
user_id INTEGER,
series TEXT NOT NULL,
series_name TEXT,
name TEXT NOT NULL DEFAULT '',
cards_json TEXT NOT NULL,
is_public INTEGER NOT NULL DEFAULT 1,
view_count INTEGER NOT NULL DEFAULT 0,
expires_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_decks_user_id ON decks(user_id);
CREATE INDEX IF NOT EXISTS idx_decks_content_hash ON decks(content_hash);
CREATE TABLE IF NOT EXISTS deck_favorites (
user_id INTEGER NOT NULL,
share_code TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (user_id, share_code),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (share_code) REFERENCES decks(share_code) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_deck_favorites_share_code ON deck_favorites(share_code);
`);
const deckCols = db.prepare('PRAGMA table_info(decks)').all();
if (!deckCols.some((c) => c.name === 'preview_image')) {
db.exec('ALTER TABLE decks ADD COLUMN preview_image TEXT');
}
const userCols = db.prepare('PRAGMA table_info(users)').all();
if (!userCols.some((c) => c.name === 'default_deck_public')) {
db.exec('ALTER TABLE users ADD COLUMN default_deck_public INTEGER NOT NULL DEFAULT 1');
}
if (!userCols.some((c) => c.name === 'skip_deck_public_prompt')) {
db.exec('ALTER TABLE users ADD COLUMN skip_deck_public_prompt INTEGER NOT NULL DEFAULT 0');
}
if (!userCols.some((c) => c.name === 'last_login_at')) {
db.exec('ALTER TABLE users ADD COLUMN last_login_at TEXT');
}
return db;
}
/**
* 환경변수 기준 관리자 계정 시드
* ADMIN_EMAIL로 이미 가입한 경우 역할만 admin으로 승격한다.
* @param {import('better-sqlite3').Database} db
*/
export function seedAdmin(db) {
const email = process.env.ADMIN_EMAIL?.trim().toLowerCase();
const password = process.env.ADMIN_PASSWORD;
const username = process.env.ADMIN_USERNAME || 'admin';
if (!email || !password) return;
const existingAdmin = db.prepare('SELECT id FROM users WHERE role = ?').get('admin');
if (existingAdmin) return;
const existingUser = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
if (existingUser) {
db.prepare(`
UPDATE users SET role = 'admin', updated_at = datetime('now')
WHERE id = ?
`).run(existingUser.id);
return;
}
const hash = bcrypt.hashSync(password, 12);
db.prepare(`
INSERT INTO users (email, username, password_hash, display_name, role)
VALUES (?, ?, ?, ?, 'admin')
`).run(email, username, hash, '관리자');
}