diff --git a/Dockerfile b/Dockerfile index 197526a..47f72c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,4 +41,4 @@ USER node EXPOSE 3000 # Start the server -CMD ["node", "backend/dist/server.js"] +CMD ["node", "backend/dist/server.js"] \ No newline at end of file diff --git a/README.md b/README.md index 23bf78c..038c326 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,18 @@ 如果你想把数据完全掌控在自己手里,或者需要在内网环境运行。 +**⚠️ 核心安全须知**:为了提供最高级别的安全保障,本项目的 Docker 镜像严格遵循**非特权用户(Rootless)**运行标准(内部以 `node` 用户,UID 1000 运行),从而有效防止潜在的容器逃逸风险。 +在运行容器前,你**必须**预先在宿主机创建目录并分配正确的权限,否则会出现 `Permission Denied` 报错。 + +#### 0. 准备数据目录 (必做) +在你想运行容器的目录下,执行以下命令: +```bash +mkdir -p data && sudo chown -R 1000:1000 data +``` + #### 1. 使用 Docker Compose (推荐) -在你的服务器上创建 `docker-compose.yml`: + +1. 在服务器上创建 `docker-compose.yml`: ```yaml services: 2fauth: @@ -103,6 +113,7 @@ services: volumes: - ./data:/app/data environment: + # 核心密钥 - ENCRYPTION_KEY=32位以上随机密钥 - JWT_SECRET=32位以上随机JWT密钥 - OAUTH_ALLOWED_USERS=你的邮箱@example.com @@ -112,7 +123,7 @@ services: - OAUTH_GITHUB_REDIRECT_URI=https://你的域名/oauth/callback restart: unless-stopped ``` -运行 `docker compose up -d` 即可。 +2. 运行 `docker compose up -d` 即可。 #### 2. 使用 Docker Run ```bash @@ -124,8 +135,8 @@ docker run -d --name 2fauth-worker \ -e OAUTH_ALLOWED_USERS=你的邮箱 \ -e OAUTH_GITHUB_CLIENT_ID= \ -e OAUTH_GITHUB_CLIENT_SECRET= \ - -e OAUTH_GITHUB_CLIENT_REDIRECT_URI= \ - nap0o/2fauth-worker:latest + -e OAUTH_GITHUB_REDIRECT_URI=https://你的域名/oauth/callback \ + nap0o/2fauth-worker:latest ``` --- diff --git a/README_EN.md b/README_EN.md index 6930f35..3facc11 100644 --- a/README_EN.md +++ b/README_EN.md @@ -93,8 +93,17 @@ The most hassle-free way. No server required; Cloudflare hosts it for you for fr If you want absolute control over your data or need to run it in a local network. +**⚠️ Important Security Notice**: To provide the highest level of security, this image runs strictly as a **non-privileged user (node, UID 1000)** to prevent container escape risks. +You **MUST** create the data directory and set the correct permissions on your host machine before starting the container, otherwise you will encounter `Permission Denied` errors. + +#### 0. Prepare Data Directory (Required) +```bash +mkdir -p data && sudo chown -R 1000:1000 data +``` + #### 1. Using Docker Compose (Recommended) -Create `docker-compose.yml` on your server: + +1. Create `docker-compose.yml` on your server: ```yaml services: 2fauth: @@ -105,16 +114,17 @@ services: volumes: - ./data:/app/data environment: + # Core Secrets - ENCRYPTION_KEY=Your_32_Character_Random_Key - - JWT_SECRET=Your_Random_JWT_Secret + - JWT_SECRET=Your_32_Character_Random_JWT_Secret - OAUTH_ALLOWED_USERS=your_email@example.com # Configure at least one provider (GitHub example) - OAUTH_GITHUB_CLIENT_ID=your_id - OAUTH_GITHUB_CLIENT_SECRET=your_secret - - OAUTH_GITHUB_REDIRECT_URI=https://your-domain.com/oauth/callback + - OAUTH_GITHUB_REDIRECT_URI=[https://your-domain.com/oauth/callback](https://your-domain.com/oauth/callback) restart: unless-stopped ``` -Run `docker compose up -d`. +2. Run `docker compose up -d`. #### 2. Using Docker Run ```bash @@ -126,8 +136,8 @@ docker run -d --name 2fauth-worker \ -e OAUTH_ALLOWED_USERS=your_email@example.com \ -e OAUTH_GITHUB_CLIENT_ID= \ -e OAUTH_GITHUB_CLIENT_SECRET= \ - -e OAUTH_GITHUB_CLIENT_REDIRECT_URI= \ - nap0o/2fauth-worker:latest + -e OAUTH_GITHUB_REDIRECT_URI=[https://your-domain.com/oauth/callback](https://your-domain.com/oauth/callback) \ + nap0o/2fauth-worker:latest ``` --- diff --git a/backend/package.json b/backend/package.json index 5100edc..1e5251b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,7 +14,7 @@ "drizzle-orm": "^0.30.10", "hono": "^4.12.3", "node-cron": "^4.2.1", - "nodemailer": "^6.10.1", + "nodemailer": "^7.0.11", "webdav": "^5.9.0" }, "devDependencies": { @@ -22,7 +22,7 @@ "@types/better-sqlite3": "^7.6.13", "@types/node": "^25.3.5", "@types/node-cron": "^3.0.11", - "@types/nodemailer": "^6.4.17", + "@types/nodemailer": "^7.0.11", "drizzle-kit": "^0.31.9", "typescript": "^5.9.3", "wrangler": "^4.71.0" diff --git a/backend/src/app/server.ts b/backend/src/app/server.ts index 505826a..823b117 100644 --- a/backend/src/app/server.ts +++ b/backend/src/app/server.ts @@ -18,14 +18,46 @@ const dataDir = path.resolve(baseDir, 'data'); console.log(`[Docker Server] Base directory: ${baseDir}`); console.log(`[Docker Server] Frontend dist path: ${frontendDistPath}`); -// 2. Ensure data directory exists +// 2. Ensure data directory exists and is writable if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } +try { + fs.accessSync(dataDir, fs.constants.W_OK); +} catch (err) { + console.error(`\n❌ ERROR: Data directory "${dataDir}" is NOT writable!`); + console.error(` Please run on your host: sudo chown -R 1000:1000 ./data\n`); + process.exit(1); +} + // 3. Initialize local SQLite Database const dbFile = process.env.SQLITE_DB_PATH || path.join(dataDir, '2fauth.db'); -const sqlite = new Database(dbFile); + +// Check if database file is writable if it exists +if (fs.existsSync(dbFile)) { + try { + fs.accessSync(dbFile, fs.constants.W_OK); + } catch (err) { + console.error(`\n❌ ERROR: Database file "${dbFile}" is NOT writable!`); + console.error(` Please run on your host: sudo chown 1000:1000 ${dbFile}\n`); + process.exit(1); + } +} + +let sqlite: Database.Database; +try { + sqlite = new Database(dbFile); +} catch (err: any) { + if (err.code === 'SQLITE_CANTOPEN') { + console.error(`\n❌ ERROR: SQLite cannot open database file "${dbFile}".`); + console.error(` This is likely a permission issue on the mounted volume.`); + console.error(` Please run on your host: sudo chown -R 1000:1000 ./data\n`); + } else { + console.error(`\n❌ ERROR: Failed to open database:`, err.message); + } + process.exit(1); +} sqlite.pragma('journal_mode = WAL'); // 4. Initialize Drizzle ORM diff --git a/backend/src/features/auth/authRoutes.ts b/backend/src/features/auth/authRoutes.ts index d00e57f..6c9c21a 100644 --- a/backend/src/features/auth/authRoutes.ts +++ b/backend/src/features/auth/authRoutes.ts @@ -9,7 +9,7 @@ import { WebAuthnService } from '@/features/auth/webAuthnService'; const auth = new Hono<{ Bindings: EnvBindings, Variables: { user: any } }>(); const getService = (c: any) => new AuthService(c.env); -const getWebAuthnService = (c: any) => new WebAuthnService(c.env, c.req.url); +const getWebAuthnService = (c: any) => new WebAuthnService(c.env, c.req.url, c.req.header()); // 获取可用登录方式列表 auth.get('/providers', (c) => { diff --git a/backend/src/features/auth/webAuthnService.ts b/backend/src/features/auth/webAuthnService.ts index eda60be..13dd343 100644 --- a/backend/src/features/auth/webAuthnService.ts +++ b/backend/src/features/auth/webAuthnService.ts @@ -15,11 +15,17 @@ export class WebAuthnService { private rpID: string; private origin: string; - constructor(env: EnvBindings, url: string) { + constructor(env: EnvBindings, url: string, headers?: Record) { this.env = env; const parsedUrl = new URL(url); - this.rpID = parsedUrl.hostname; - this.origin = `${parsedUrl.protocol}//${parsedUrl.host}`; + + // Handle Reverse Proxy (Docker/Nginx/Cloudflare) + // If X-Forwarded-Proto exists, use it as the real protocol + const proto = headers?.['x-forwarded-proto'] || parsedUrl.protocol.replace(':', ''); + const host = headers?.['x-forwarded-host'] || parsedUrl.host; + + this.rpID = host.split(':')[0]; // Domain only for RP ID + this.origin = `${proto.includes('://') ? proto : proto + '://'}${host}`; } /** diff --git a/docker-compose.yml b/docker-compose.yml index b2a5d40..c6c46b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,12 +25,12 @@ services: # Google 登录配置 # - OAUTH_GOOGLE_CLIENT_ID= # - OAUTH_GOOGLE_CLIENT_SECRET= - # - OAUTH_GOOGLE_REDIRECT_URI=https:///api/oauth/callback + # - OAUTH_GOOGLE_REDIRECT_URI=https:///oauth/callback # Github 登录配置 # - OAUTH_GITHUB_CLIENT_ID= # - OAUTH_GITHUB_CLIENT_SECRET= - # - OAUTH_GITHUB_REDIRECT_URI=https:///api/oauth/callback + # - OAUTH_GITHUB_REDIRECT_URI=https:///oauth/callback # Telegram 登录配置 # - OAUTH_TELEGRAM_BOT_NAME=your_bot_name @@ -50,4 +50,4 @@ services: # Nodeloc 登录配置 # - OAUTH_NODELOC_CLIENT_ID= # - OAUTH_NODELOC_CLIENT_SECRET= - # - OAUTH_NODELOC_REDIRECT_URI=https:///api/oauth/callback + # - OAUTH_NODELOC_REDIRECT_URI=https:///oauth/callback \ No newline at end of file