Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,33 @@ COS_BUCKET=your-bucket-name
COS_REGION=ap-guangzhou

# Application Configuration
# 端口配置说明:
# - 如需并行运行多个项目实例,请修改以下端口配置
# - 建议使用不同的端口范围,例如:
# - 实例1: FRONTEND_PORT=8001, BACKEND_PORT=8002
# - 实例2: FRONTEND_PORT=8011, BACKEND_PORT=8012
# - 实例3: FRONTEND_PORT=8021, BACKEND_PORT=8022
#
# 注意:修改端口后,需要同时更新以下配置:
# - NEXT_PUBLIC_API_URL (前端调用后端的地址)
# - CORS_ORIGIN (后端允许的前端地址)
# - AUTH_BASE_URL (认证基础 URL)
BACKEND_PORT=8002
FRONTEND_PORT=8001
NODE_ENV=development

# CORS
# Cors (production example: https://novel.daerai.com)
# 后端允许的前端地址列表,多个地址用逗号分隔
# 开发环境示例: http://localhost:8001
# 生产环境示例: https://novel.daerai.com
# 如需并行运行多个项目,需要添加对应前端地址
CORS_ORIGIN=http://localhost:8001,https://novel.daerai.com

# Authentication Base URL (MUST be set in production)
# Example: https://novel.daerai.com/api/auth
# 认证服务的基础 URL,用于 OAuth 回调等
# 开发环境示例: http://localhost:8002/api/auth
# 生产环境示例: https://novel.daerai.com/api/auth
# 如需并行运行多个项目,需要修改为对应后端端口
AUTH_BASE_URL=http://localhost:8002/api/auth

# LinuxDo OAuth
Expand Down
256 changes: 144 additions & 112 deletions apps/backend/src/config/auth.config.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
import { betterAuth } from 'better-auth';
import { genericOAuth } from "better-auth/plugins"
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from '../database';
import * as schema from '../database/schema';
import { emailService } from '../services/email.service';
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../database";
import * as schema from "../database/schema";
import { emailService } from "../services/email.service";

// Validate LinuxDo OAuth environment variables
const LINUXDO_CLIENT_ID = process.env.LINUXDO_CLIENT_ID;
const LINUXDO_CLIENT_SECRET = process.env.LINUXDO_CLIENT_SECRET;

// 动态构建默认的前端 URL
const frontendPort = process.env.FRONTEND_PORT || 8001;
const defaultFrontendUrl = `http://localhost:${frontendPort}`;

if (!LINUXDO_CLIENT_ID || !LINUXDO_CLIENT_SECRET) {
console.warn('[Auth Config] ⚠️ LinuxDo OAuth is not fully configured. Missing CLIENT_ID or CLIENT_SECRET.');
console.warn('[Auth Config] LinuxDo authentication will not be available until these are set.');
console.warn(
"[Auth Config] ⚠️ LinuxDo OAuth is not fully configured. Missing CLIENT_ID or CLIENT_SECRET.",
);
console.warn(
"[Auth Config] LinuxDo authentication will not be available until these are set.",
);
}

export const auth: any = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
provider: "pg",
schema: {
user: schema.user,
session: schema.session,
account: schema.account,
verification: schema.verification,
},
}),

emailAndPassword: {
enabled: true,
requireEmailVerification: false,
requireEmailVerification: false,
minPasswordLength: 6,
maxPasswordLength: 32,
sendResetPassword: async (user, url) => {
await emailService.sendPasswordResetEmail(user.email, url);
},

},

emailVerification: {
Expand All @@ -44,92 +51,113 @@ export const auth: any = betterAuth({
},

plugins: [
...(LINUXDO_CLIENT_ID && LINUXDO_CLIENT_SECRET ? [
genericOAuth({
config: [
{
providerId: "linuxdo",
clientId: LINUXDO_CLIENT_ID,
clientSecret: LINUXDO_CLIENT_SECRET,
authorizationUrl: "https://connect.linux.do/oauth2/authorize",
tokenUrl: "https://connect.linux.do/oauth2/token",
userInfoUrl: "https://connect.linux.do/api/user",
scopes: ["openid", "profile", "email"],
getUserInfo: async (tokens) => {
const response = await fetch("https://connect.linux.do/api/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`
}
});
const user: any = await response.json();
console.log('-------- LinuxDo User Info:', user);
const now = new Date();
return {
id: String(user.id || user.sub),
name: user.name || user.username || user.login || 'LinuxDo User',
email: user.email,
emailVerified: true,
image: user.avatar_url || user.picture,
createdAt: now,
updatedAt: now,
};
}
},
// Convert GitHub to Generic OAuth to bypass PKCE issues
{
providerId: "github",
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
scopes: ["user:email"],
pkce: false, // Explicitly disable PKCE to avoid code_verifier errors
getUserInfo: async (tokens) => {
// 1. Get User Profile
const userRes = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
"User-Agent": "Daer-Novel-App"
}
});
const user: any = await userRes.json();

// 2. Get User Email (if not public)
let email = user.email;
if (!email) {
const emailRes = await fetch("https://api.github.com/user/emails", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
"User-Agent": "Daer-Novel-App"
}
});
const emails = await emailRes.json() as any[];
const primary = emails.find((e: any) => e.primary && e.verified);
if (primary) email = primary.email;
else if (emails.length > 0) email = emails[0].email;
}

console.log('-------- GitHub User Info:', { id: user.id, login: user.login, email });
const now = new Date();
return {
id: String(user.id),
name: user.name || user.login,
email: email,
emailVerified: true,
image: user.avatar_url,
createdAt: now,
updatedAt: now,
};
}
}
...(LINUXDO_CLIENT_ID && LINUXDO_CLIENT_SECRET
? [
genericOAuth({
config: [
{
providerId: "linuxdo",
clientId: LINUXDO_CLIENT_ID,
clientSecret: LINUXDO_CLIENT_SECRET,
authorizationUrl: "https://connect.linux.do/oauth2/authorize",
tokenUrl: "https://connect.linux.do/oauth2/token",
userInfoUrl: "https://connect.linux.do/api/user",
scopes: ["openid", "profile", "email"],
getUserInfo: async (tokens) => {
const response = await fetch(
"https://connect.linux.do/api/user",
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
},
);
const user: any = await response.json();
console.log("-------- LinuxDo User Info:", user);
const now = new Date();
return {
id: String(user.id || user.sub),
name:
user.name ||
user.username ||
user.login ||
"LinuxDo User",
email: user.email,
emailVerified: true,
image: user.avatar_url || user.picture,
createdAt: now,
updatedAt: now,
};
},
},
// Convert GitHub to Generic OAuth to bypass PKCE issues
{
providerId: "github",
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
authorizationUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
scopes: ["user:email"],
pkce: false, // Explicitly disable PKCE to avoid code_verifier errors
getUserInfo: async (tokens) => {
// 1. Get User Profile
const userRes = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
"User-Agent": "Daer-Novel-App",
},
});
const user: any = await userRes.json();

// 2. Get User Email (if not public)
let email = user.email;
if (!email) {
const emailRes = await fetch(
"https://api.github.com/user/emails",
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
"User-Agent": "Daer-Novel-App",
},
},
);
const emails = (await emailRes.json()) as any[];
const primary = emails.find(
(e: any) => e.primary && e.verified,
);
if (primary) email = primary.email;
else if (emails.length > 0) email = emails[0].email;
}

console.log("-------- GitHub User Info:", {
id: user.id,
login: user.login,
email,
});
const now = new Date();
return {
id: String(user.id),
name: user.name || user.login,
email: email,
emailVerified: true,
image: user.avatar_url,
createdAt: now,
updatedAt: now,
};
},
},
],
}),
]
})
] : []),
: []),
],

baseURL: process.env.AUTH_BASE_URL || process.env.BASE_URL || 'http://localhost:8002/api/auth',


baseURL:
process.env.AUTH_BASE_URL ||
process.env.BASE_URL ||
"http://localhost:8002/api/auth",

socialProviders: {
// github: { // Moved to genericOAuth
// clientId: process.env.GITHUB_CLIENT_ID!,
Expand All @@ -140,31 +168,33 @@ export const auth: any = betterAuth({
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},

session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // Update session every 24 hours
},

secret: process.env.BETTER_AUTH_SECRET!,
trustedOrigins: [
'http://localhost:8001',
'tauri://localhost',
'http://tauri.localhost',
...(process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',').map(o => o.trim()) : []),
defaultFrontendUrl,
"tauri://localhost",
"http://tauri.localhost",
...(process.env.CORS_ORIGIN
? process.env.CORS_ORIGIN.split(",").map((o) => o.trim())
: []),
],

advanced: {
generateId: () => crypto.randomUUID(),
generateId: () => crypto.randomUUID(),
useSecureCookies: false, // process.env.NODE_ENV === 'production', // DEBUG: Temporarily disable secure cookies
defaultCookieAttributes: {
sameSite: 'lax',
sameSite: "lax",
secure: false, // process.env.NODE_ENV === 'production', // DEBUG
httpOnly: true,
path: '/',
httpOnly: true,
path: "/",
},
},

onEvent: {
async sessionCreated(data: any) {
console.log(`[Auth] Session created for user: ${data.session.userId}`);
Expand All @@ -179,7 +209,7 @@ export const auth: any = betterAuth({
console.error("[Auth] Error:", context.error);
},
},

// Important for proxies: Trust the host header
trustHost: true,
});
Expand All @@ -188,8 +218,10 @@ export type Session = typeof auth.$Infer.Session;
export type User = typeof auth.$Infer.Session.user;

// Log the available OAuth providers for debugging
console.log('[Auth Config] Better Auth initialized');
console.log('[Auth Config] Configured OAuth providers:', {
console.log("[Auth Config] Better Auth initialized");
console.log("[Auth Config] Configured OAuth providers:", {
hasLinuxDo: !!(LINUXDO_CLIENT_ID && LINUXDO_CLIENT_SECRET),
linuxDoClientId: LINUXDO_CLIENT_ID ? `${LINUXDO_CLIENT_ID.substring(0, 8)}...` : 'NOT SET'
linuxDoClientId: LINUXDO_CLIENT_ID
? `${LINUXDO_CLIENT_ID.substring(0, 8)}...`
: "NOT SET",
});
4 changes: 3 additions & 1 deletion apps/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ import sandboxRoutes from './routes/sandbox.routes';
const app: Application = express();
const httpServer = createServer(app);
// CORS configuration
const frontendPort = process.env.FRONTEND_PORT || 8001;
const defaultFrontendUrl = `http://localhost:${frontendPort}`;
const allowedOrigins = process.env.CORS_ORIGIN
? process.env.CORS_ORIGIN.split(',').map(origin => origin.trim())
: ['http://localhost:8001', 'tauri://localhost', 'http://tauri.localhost'];
: [defaultFrontendUrl, 'tauri://localhost', 'http://tauri.localhost'];

const corsOptions = {
origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
Expand Down
9 changes: 9 additions & 0 deletions apps/frontend/.env.local.example
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
# 前端调用后端 API 的地址
# 开发环境示例: http://localhost:8002
# 生产环境示例: https://novel.daerai.com
#
# 如需并行运行多个项目实例,需要修改为对应后端端口
# 例如:
# - 实例1: NEXT_PUBLIC_API_URL=http://localhost:8002
# - 实例2: NEXT_PUBLIC_API_URL=http://localhost:8012
# - 实例3: NEXT_PUBLIC_API_URL=http://localhost:8022
NEXT_PUBLIC_API_URL=http://localhost:8002
2 changes: 1 addition & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 8001",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
Expand Down
Loading