first commit

This commit is contained in:
2026-04-17 16:27:18 +08:00
commit 2138e1cc0d
51 changed files with 2125 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
# Casdoor配置
CASDOOR_SERVER_URL=http://localhost:8000
CASDOOR_CLIENT_ID=your-client-id
CASDOOR_CLIENT_SECRET=your-client-secret
CASDOOR_ORG_NAME=imtim
CASDOOR_APP_NAME=base-imtim
CASDOOR_REDIRECT_URI=http://localhost:3000/api/auth/callback
# JWT配置
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
# 应用配置
APP_PORT=3001
NODE_ENV=development
FRONTEND_URL=http://localhost:3000
# 数据库
DATABASE_URL=file:./prisma/dev.db
+29
View File
@@ -0,0 +1,29 @@
{
"name": "backend",
"version": "1.0.0",
"description": "Koa backend for IMTIM Portal",
"main": "src/index.js",
"type": "module",
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js",
"db:migrate": "prisma migrate dev",
"db:generate": "prisma generate",
"db:studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"axios": "^1.15.0",
"casdoor-nodejs-sdk": "^1.27.0",
"dotenv": "^16.4.5",
"jsonwebtoken": "^9.0.2",
"koa": "^2.15.3",
"koa-bodyparser": "^4.4.1",
"koa-cors": "^0.0.16",
"koa-router": "^12.0.1"
},
"devDependencies": {
"nodemon": "^3.1.7",
"prisma": "^5.22.0"
}
}
@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"email" TEXT,
"avatar" TEXT,
"casdoorId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "User_casdoorId_key" ON "User"("casdoorId");
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"
+19
View File
@@ -0,0 +1,19 @@
// Prisma Schema
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(uuid())
username String @unique
email String? @unique
avatar String?
casdoorId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+5
View File
@@ -0,0 +1,5 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default prisma;
+21
View File
@@ -0,0 +1,21 @@
import dotenv from 'dotenv';
dotenv.config();
export const config = {
casdoor: {
serverUrl: process.env.CASDOOR_SERVER_URL,
clientId: process.env.CASDOOR_CLIENT_ID,
clientSecret: process.env.CASDOOR_CLIENT_SECRET,
orgName: process.env.CASDOOR_ORG_NAME,
appName: process.env.CASDOOR_APP_NAME,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '7d',
},
app: {
port: parseInt(process.env.APP_PORT || '3001', 10),
env: process.env.NODE_ENV || 'development',
},
};
@@ -0,0 +1,99 @@
import { getLoginUrl, handleCallback } from '../services/auth.service.js';
import prisma from '../config/database.js';
/**
* POST /api/auth/login
* 获取 Casdoor 登录 URL
*/
export const login = (ctx) => {
const loginUrl = getLoginUrl();
ctx.body = {
success: true,
data: { loginUrl },
};
};
/**
* GET /api/auth/callback
* 处理 Casdoor OAuth 回调
*/
export const callback = async (ctx) => {
const { code } = ctx.query;
if (!code) {
ctx.status = 400;
ctx.body = {
success: false,
message: 'Missing authorization code',
};
return;
}
try {
const redirectUri = process.env.CASDOOR_REDIRECT_URI || 'http://localhost:3000/api/auth/callback';
const { user, token } = await handleCallback(code, redirectUri);
// 重定向到前端仪表板,携带 token
ctx.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3000'}/dashboard?token=${token}`);
} catch (error) {
ctx.status = 401;
ctx.body = {
success: false,
message: error.message,
};
}
};
/**
* POST /api/auth/logout
* 登出
*/
export const logout = (ctx) => {
// JWT 是无状态的,客户端删除 token 即可
ctx.body = {
success: true,
message: 'Logged out successfully',
};
};
/**
* GET /api/auth/user
* 获取当前用户信息
*/
export const getUser = async (ctx) => {
try {
const userId = ctx.state.user.userId;
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
username: true,
email: true,
avatar: true,
createdAt: true,
updatedAt: true,
},
});
if (!user) {
ctx.status = 404;
ctx.body = {
success: false,
message: 'User not found',
};
return;
}
ctx.body = {
success: true,
data: user,
};
} catch (error) {
ctx.status = 500;
ctx.body = {
success: false,
message: 'Failed to fetch user info',
};
}
};
+49
View File
@@ -0,0 +1,49 @@
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import cors from 'koa-cors';
import { config } from './config/index.js';
import authRoutes from './routes/auth.routes.js';
import healthRoutes from './routes/health.routes.js';
const app = new Koa();
// 中间件
app.use(cors());
app.use(bodyParser());
// 日志中间件
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.path} - ${ms}ms`);
});
// 错误处理中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
console.error('Server error:', error);
ctx.status = error.status || 500;
ctx.body = {
success: false,
message: error.message || 'Internal Server Error',
};
}
});
// 路由
app.use(authRoutes.routes());
app.use(authRoutes.allowedMethods());
app.use(healthRoutes.routes());
app.use(healthRoutes.allowedMethods());
// 启动服务器
const PORT = config.app.port;
app.listen(PORT, () => {
console.log(`🚀 Backend server running on http://localhost:${PORT}`);
console.log(`📊 Environment: ${config.app.env}`);
});
export default app;
+32
View File
@@ -0,0 +1,32 @@
import { verifyToken } from '../services/auth.service.js';
/**
* 认证中间件 - 验证 JWT token
*/
export const authMiddleware = async (ctx, next) => {
const authHeader = ctx.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
ctx.status = 401;
ctx.body = {
success: false,
message: 'Unauthorized: No token provided',
};
return;
}
const token = authHeader.split(' ')[1];
const decoded = verifyToken(token);
if (!decoded) {
ctx.status = 401;
ctx.body = {
success: false,
message: 'Unauthorized: Invalid token',
};
return;
}
ctx.state.user = decoded;
await next();
};
+15
View File
@@ -0,0 +1,15 @@
import Router from 'koa-router';
import { login, callback, logout, getUser } from '../controllers/auth.controller.js';
import { authMiddleware } from '../middleware/auth.js';
const router = new Router({ prefix: '/api/auth' });
// 公开路由
router.post('/login', login);
router.get('/callback', callback);
router.post('/logout', logout);
// 需要认证的路由
router.get('/user', authMiddleware, getUser);
export default router;
+13
View File
@@ -0,0 +1,13 @@
import Router from 'koa-router';
const router = new Router({ prefix: '/api' });
router.get('/health', (ctx) => {
ctx.body = {
success: true,
message: 'OK',
timestamp: new Date().toISOString(),
};
});
export default router;
+109
View File
@@ -0,0 +1,109 @@
import axios from 'axios';
import jwt from 'jsonwebtoken';
import { config } from '../config/index.js';
import prisma from '../config/database.js';
/**
* 生成 Casdoor 登录 URL
*/
export const getLoginUrl = () => {
const { serverUrl, clientId, appName, orgName } = config.casdoor;
// Casdoor回调到前端3000端口,middleware代理转发到后端
const redirectUri = process.env.CASDOOR_REDIRECT_URI || 'http://localhost:3000/api/auth/callback';
const state = 'imtim-portal';
return `${serverUrl}/login/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=read&state=${state}&org=${orgName}&app=${appName}`;
};
/**
* 处理 Casdoor OAuth 回调
*/
export const handleCallback = async (code, redirectUri) => {
try {
// 用 code 换取 token
const tokenResponse = await axios.post(
`${config.casdoor.serverUrl}/api/login/oauth/access_token`,
new URLSearchParams({
grant_type: 'authorization_code',
client_id: config.casdoor.clientId,
client_secret: config.casdoor.clientSecret,
code: code,
redirect_uri: redirectUri,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
const { access_token } = tokenResponse.data;
if (!access_token) {
console.error('Token response:', tokenResponse.data);
throw new Error('Failed to get access token');
}
// 获取用户信息 - Casdoor v3.x 使用 /api/get-account
const userResponse = await axios.get(
`${config.casdoor.serverUrl}/api/get-account`,
{
headers: {
'Authorization': `Bearer ${access_token}`,
},
}
);
console.log('Casdoor get-account response:', JSON.stringify(userResponse.data, null, 2));
const userInfo = userResponse.data.data;
if (!userInfo || !userInfo.id) {
console.error('User info missing. Got:', userInfo);
throw new Error('Failed to get user info');
}
// 创建或更新本地用户
const user = await prisma.user.upsert({
where: { casdoorId: userInfo.id },
update: {
username: userInfo.name || userInfo.displayName,
email: userInfo.email,
avatar: userInfo.avatar,
},
create: {
username: userInfo.name || userInfo.displayName || 'user',
email: userInfo.email || null,
casdoorId: userInfo.id,
avatar: userInfo.avatar || null,
},
});
// 生成 JWT
const jwtToken = jwt.sign(
{
userId: user.id,
username: user.username,
casdoorId: user.casdoorId,
},
config.jwt.secret,
{ expiresIn: config.jwt.expiresIn }
);
return { user, token: jwtToken };
} catch (error) {
console.error('OAuth callback error:', error.response?.data || error.message);
throw new Error(`Authentication failed: ${error.message}`);
}
};
/**
* 验证 JWT token
*/
export const verifyToken = (token) => {
try {
return jwt.verify(token, config.jwt.secret);
} catch (error) {
return null;
}
};