first commit
This commit is contained in:
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default prisma;
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user