mern-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MERN Stack Patterns Skill

MERN栈开发模式技能

Comprehensive MERN stack development patterns for the keycloak-alpha multi-tenant platform with 8 microservices.
为拥有8个微服务的keycloak-alpha多租户平台提供全面的MERN栈开发模式。

When to Use This Skill

何时使用该技能

Activate this skill when:
  • Building React + Vite frontend applications
  • Implementing Express microservices
  • Designing MongoDB schemas and data models
  • Setting up API Gateway architecture
  • Implementing session and cookie management
  • Adding error handling and validation
  • Writing tests for MERN stack applications
在以下场景中启用该技能:
  • 构建React + Vite前端应用
  • 实现Express微服务
  • 设计MongoDB Schema与数据模型
  • 搭建API网关架构
  • 实现会话与Cookie管理
  • 添加错误处理与校验逻辑
  • 为MERN栈应用编写测试用例

keycloak-alpha Project Structure

keycloak-alpha项目结构

keycloak-alpha/
├── apps/
│   ├── web-app/                    # Main React + Vite SPA
│   │   ├── src/
│   │   │   ├── components/
│   │   │   ├── pages/
│   │   │   ├── hooks/
│   │   │   ├── contexts/
│   │   │   ├── config/
│   │   │   ├── utils/
│   │   │   └── main.jsx
│   │   ├── vite.config.js
│   │   └── package.json
│   └── admin-portal/               # Admin dashboard (React + Vite)
├── services/
│   ├── api-gateway/                # Express API Gateway
│   ├── user-service/               # User management
│   ├── org-service/                # Organization management
│   ├── tenant-service/             # Multi-tenant provisioning
│   ├── notification-service/       # Email/SMS notifications
│   ├── billing-service/            # Stripe integration
│   ├── analytics-service/          # Usage analytics
│   └── keycloak-service/          # Keycloak integration
├── routes/
│   ├── api/
│   │   ├── users.js
│   │   ├── organizations.js
│   │   └── tenants.js
│   └── index.js
├── shared/
│   ├── types/
│   ├── utils/
│   └── constants/
└── package.json
keycloak-alpha/
├── apps/
│   ├── web-app/                    # 主React + Vite单页应用
│   │   ├── src/
│   │   │   ├── components/
│   │   │   ├── pages/
│   │   │   ├── hooks/
│   │   │   ├── contexts/
│   │   │   ├── config/
│   │   │   ├── utils/
│   │   │   └── main.jsx
│   │   ├── vite.config.js
│   │   └── package.json
│   └── admin-portal/               # 管理后台(React + Vite)
├── services/
│   ├── api-gateway/                # Express API网关
│   ├── user-service/               # 用户管理服务
│   ├── org-service/                # 组织管理服务
│   ├── tenant-service/             # 多租户配置服务
│   ├── notification-service/       # 邮件/短信通知服务
│   ├── billing-service/            # Stripe集成服务
│   ├── analytics-service/          # 使用情况分析服务
│   └── keycloak-service/          # Keycloak集成服务
├── routes/
│   ├── api/
│   │   ├── users.js
│   │   ├── organizations.js
│   │   └── tenants.js
│   └── index.js
├── shared/
│   ├── types/
│   ├── utils/
│   └── constants/
└── package.json

React + Vite Frontend Patterns

React + Vite前端模式

Vite Configuration

Vite配置

javascript
// apps/web-app/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],

  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
      '@contexts': path.resolve(__dirname, './src/contexts'),
      '@utils': path.resolve(__dirname, './src/utils'),
      '@config': path.resolve(__dirname, './src/config'),
    }
  },

  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:4000',
        changeOrigin: true,
      }
    }
  },

  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['react', 'react-dom', 'react-router-dom'],
          'keycloak': ['keycloak-js'],
          'ui': ['@chakra-ui/react', '@emotion/react']
        }
      }
    }
  },

  optimizeDeps: {
    include: ['react', 'react-dom', 'keycloak-js']
  }
});
javascript
// apps/web-app/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],

  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
      '@contexts': path.resolve(__dirname, './src/contexts'),
      '@utils': path.resolve(__dirname, './src/utils'),
      '@config': path.resolve(__dirname, './src/config'),
    }
  },

  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:4000',
        changeOrigin: true,
      }
    }
  },

  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['react', 'react-dom', 'react-router-dom'],
          'keycloak': ['keycloak-js'],
          'ui': ['@chakra-ui/react', '@emotion/react']
        }
      }
    }
  },

  optimizeDeps: {
    include: ['react', 'react-dom', 'keycloak-js']
  }
});

Component Organization

组件组织

javascript
// apps/web-app/src/components/features/UserProfile/index.jsx
import { useState, useEffect } from 'react';
import { useAuth } from '@hooks/useAuth';
import { useToast } from '@chakra-ui/react';
import { updateUserProfile } from '@/api/users';

export function UserProfile() {
  const { user } = useAuth();
  const toast = useToast();
  const [profile, setProfile] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    fetchProfile();
  }, []);

  async function fetchProfile() {
    setLoading(true);
    try {
      const data = await getUserProfile(user.sub);
      setProfile(data);
    } catch (error) {
      toast({
        title: 'Error loading profile',
        description: error.message,
        status: 'error',
        duration: 5000,
      });
    } finally {
      setLoading(false);
    }
  }

  async function handleSubmit(formData) {
    try {
      await updateUserProfile(user.sub, formData);
      toast({
        title: 'Profile updated',
        status: 'success',
        duration: 3000,
      });
    } catch (error) {
      toast({
        title: 'Update failed',
        description: error.message,
        status: 'error',
        duration: 5000,
      });
    }
  }

  if (loading) return <Spinner />;
  if (!profile) return <Alert>Profile not found</Alert>;

  return <ProfileForm profile={profile} onSubmit={handleSubmit} />;
}
javascript
// apps/web-app/src/components/features/UserProfile/index.jsx
import { useState, useEffect } from 'react';
import { useAuth } from '@hooks/useAuth';
import { useToast } from '@chakra-ui/react';
import { updateUserProfile } from '@/api/users';

export function UserProfile() {
  const { user } = useAuth();
  const toast = useToast();
  const [profile, setProfile] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    fetchProfile();
  }, []);

  async function fetchProfile() {
    setLoading(true);
    try {
      const data = await getUserProfile(user.sub);
      setProfile(data);
    } catch (error) {
      toast({
        title: '加载个人资料失败',
        description: error.message,
        status: 'error',
        duration: 5000,
      });
    } finally {
      setLoading(false);
    }
  }

  async function handleSubmit(formData) {
    try {
      await updateUserProfile(user.sub, formData);
      toast({
        title: '个人资料更新成功',
        status: 'success',
        duration: 3000,
      });
    } catch (error) {
      toast({
        title: '更新失败',
        description: error.message,
        status: 'error',
        duration: 5000,
      });
    }
  }

  if (loading) return <Spinner />;
  if (!profile) return <Alert>未找到个人资料</Alert>;

  return <ProfileForm profile={profile} onSubmit={handleSubmit} />;
}

Custom Hooks Pattern

自定义Hook模式

javascript
// apps/web-app/src/hooks/useAuth.js
import { createContext, useContext, useState, useEffect } from 'react';
import Keycloak from 'keycloak-js';
import { keycloakConfig } from '@config/keycloak.config';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [keycloak, setKeycloak] = useState(null);
  const [authenticated, setAuthenticated] = useState(false);
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const kc = new Keycloak(keycloakConfig);

    kc.init({
      onLoad: 'check-sso',
      checkLoginIframe: true,
      pkceMethod: 'S256'
    }).then(authenticated => {
      setKeycloak(kc);
      setAuthenticated(authenticated);

      if (authenticated) {
        setUser({
          sub: kc.tokenParsed.sub,
          email: kc.tokenParsed.email,
          name: kc.tokenParsed.name,
          orgId: kc.tokenParsed.org_id,
          roles: kc.tokenParsed.realm_access?.roles || []
        });
      }

      setLoading(false);
    });

    // Token refresh
    kc.onTokenExpired = () => {
      kc.updateToken(30).catch(() => {
        kc.logout();
      });
    };
  }, []);

  const login = () => keycloak.login();
  const logout = () => keycloak.logout();
  const getToken = () => keycloak.token;

  return (
    <AuthContext.Provider value={{
      authenticated,
      user,
      loading,
      login,
      logout,
      getToken
    }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};
javascript
// apps/web-app/src/hooks/useAuth.js
import { createContext, useContext, useState, useEffect } from 'react';
import Keycloak from 'keycloak-js';
import { keycloakConfig } from '@config/keycloak.config';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [keycloak, setKeycloak] = useState(null);
  const [authenticated, setAuthenticated] = useState(false);
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const kc = new Keycloak(keycloakConfig);

    kc.init({
      onLoad: 'check-sso',
      checkLoginIframe: true,
      pkceMethod: 'S256'
    }).then(authenticated => {
      setKeycloak(kc);
      setAuthenticated(authenticated);

      if (authenticated) {
        setUser({
          sub: kc.tokenParsed.sub,
          email: kc.tokenParsed.email,
          name: kc.tokenParsed.name,
          orgId: kc.tokenParsed.org_id,
          roles: kc.tokenParsed.realm_access?.roles || []
        });
      }

      setLoading(false);
    });

    // Token刷新
    kc.onTokenExpired = () => {
      kc.updateToken(30).catch(() => {
        kc.logout();
      });
    };
  }, []);

  const login = () => keycloak.login();
  const logout = () => keycloak.logout();
  const getToken = () => keycloak.token;

  return (
    <AuthContext.Provider value={{
      authenticated,
      user,
      loading,
      login,
      logout,
      getToken
    }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth必须在AuthProvider内部使用');
  }
  return context;
};

API Client Pattern

API客户端模式

javascript
// apps/web-app/src/utils/apiClient.js
import axios from 'axios';
import { useAuth } from '@hooks/useAuth';

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:4000/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// Request interceptor to add auth token
apiClient.interceptors.request.use(
  async (config) => {
    const token = await getToken();
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor for error handling
apiClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      // Redirect to login
      window.location.href = '/login';
    }

    const errorMessage = error.response?.data?.message || error.message;
    return Promise.reject(new Error(errorMessage));
  }
);

export default apiClient;

// Typed API methods
export const api = {
  users: {
    getProfile: (userId) => apiClient.get(`/users/${userId}`),
    updateProfile: (userId, data) => apiClient.put(`/users/${userId}`, data),
    listUsers: (orgId) => apiClient.get(`/users?org_id=${orgId}`)
  },
  organizations: {
    get: (orgId) => apiClient.get(`/organizations/${orgId}`),
    create: (data) => apiClient.post('/organizations', data),
    update: (orgId, data) => apiClient.put(`/organizations/${orgId}`, data)
  }
};
javascript
// apps/web-app/src/utils/apiClient.js
import axios from 'axios';
import { useAuth } from '@hooks/useAuth';

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:4000/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器:添加认证Token
apiClient.interceptors.request.use(
  async (config) => {
    const token = await getToken();
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截器:错误处理
apiClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      // 重定向到登录页
      window.location.href = '/login';
    }

    const errorMessage = error.response?.data?.message || error.message;
    return Promise.reject(new Error(errorMessage));
  }
);

export default apiClient;

// 类型化API方法
export const api = {
  users: {
    getProfile: (userId) => apiClient.get(`/users/${userId}`),
    updateProfile: (userId, data) => apiClient.put(`/users/${userId}`, data),
    listUsers: (orgId) => apiClient.get(`/users?org_id=${orgId}`)
  },
  organizations: {
    get: (orgId) => apiClient.get(`/organizations/${orgId}`),
    create: (data) => apiClient.post('/organizations', data),
    update: (orgId, data) => apiClient.put(`/organizations/${orgId}`, data)
  }
};

Express Microservice Patterns

Express微服务模式

Service Structure

服务结构

javascript
// services/user-service/src/index.js
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import morgan from 'morgan';
import { connectDB } from './config/database.js';
import { errorHandler } from './middleware/errorHandler.js';
import { authMiddleware } from './middleware/auth.js';
import userRoutes from './routes/users.js';

const app = express();

// Security middleware
app.use(helmet());
app.use(cors({
  origin: process.env.CORS_ORIGIN?.split(',') || 'http://localhost:3000',
  credentials: true
}));

// Logging
app.use(morgan('combined'));

// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Connect to MongoDB
await connectDB();

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', service: 'user-service' });
});

// API routes (protected)
app.use('/api/users', authMiddleware, userRoutes);

// Error handling
app.use(errorHandler);

const PORT = process.env.PORT || 5001;
app.listen(PORT, () => {
  console.log(`User service running on port ${PORT}`);
});
javascript
// services/user-service/src/index.js
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import morgan from 'morgan';
import { connectDB } from './config/database.js';
import { errorHandler } from './middleware/errorHandler.js';
import { authMiddleware } from './middleware/auth.js';
import userRoutes from './routes/users.js';

const app = express();

// 安全中间件
app.use(helmet());
app.use(cors({
  origin: process.env.CORS_ORIGIN?.split(',') || 'http://localhost:3000',
  credentials: true
}));

// 日志记录
app.use(morgan('combined'));

// 请求体解析
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// 连接MongoDB
await connectDB();

// 健康检查
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', service: 'user-service' });
});

// API路由(受保护)
app.use('/api/users', authMiddleware, userRoutes);

// 错误处理
app.use(errorHandler);

const PORT = process.env.PORT || 5001;
app.listen(PORT, () => {
  console.log(`用户服务运行在端口${PORT}`);
});

Route Organization

路由组织

javascript
// services/user-service/src/routes/users.js
import express from 'express';
import { body, param, query } from 'express-validator';
import { validate } from '../middleware/validate.js';
import { requireRole } from '../middleware/rbac.js';
import * as userController from '../controllers/user.controller.js';

const router = express.Router();

// GET /api/users - List users (org admin only)
router.get('/',
  requireRole(['org_admin']),
  query('org_id').optional().isString(),
  query('page').optional().isInt({ min: 1 }),
  query('limit').optional().isInt({ min: 1, max: 100 }),
  validate,
  userController.listUsers
);

// GET /api/users/:id - Get user by ID
router.get('/:id',
  param('id').isUUID(),
  validate,
  userController.getUser
);

// POST /api/users - Create user (org admin only)
router.post('/',
  requireRole(['org_admin']),
  body('email').isEmail().normalizeEmail(),
  body('firstName').trim().isLength({ min: 1, max: 50 }),
  body('lastName').trim().isLength({ min: 1, max: 50 }),
  body('orgId').isString(),
  validate,
  userController.createUser
);

// PUT /api/users/:id - Update user
router.put('/:id',
  param('id').isUUID(),
  body('firstName').optional().trim().isLength({ min: 1, max: 50 }),
  body('lastName').optional().trim().isLength({ min: 1, max: 50 }),
  validate,
  userController.updateUser
);

// DELETE /api/users/:id - Delete user (org admin only)
router.delete('/:id',
  requireRole(['org_admin']),
  param('id').isUUID(),
  validate,
  userController.deleteUser
);

export default router;
javascript
// services/user-service/src/routes/users.js
import express from 'express';
import { body, param, query } from 'express-validator';
import { validate } from '../middleware/validate.js';
import { requireRole } from '../middleware/rbac.js';
import * as userController from '../controllers/user.controller.js';

const router = express.Router();

// GET /api/users - 列出用户(仅组织管理员可用)
router.get('/',
  requireRole(['org_admin']),
  query('org_id').optional().isString(),
  query('page').optional().isInt({ min: 1 }),
  query('limit').optional().isInt({ min: 1, max: 100 }),
  validate,
  userController.listUsers
);

// GET /api/users/:id - 根据ID获取用户信息
router.get('/:id',
  param('id').isUUID(),
  validate,
  userController.getUser
);

// POST /api/users - 创建用户(仅组织管理员可用)
router.post('/',
  requireRole(['org_admin']),
  body('email').isEmail().normalizeEmail(),
  body('firstName').trim().isLength({ min: 1, max: 50 }),
  body('lastName').trim().isLength({ min: 1, max: 50 }),
  body('orgId').isString(),
  validate,
  userController.createUser
);

// PUT /api/users/:id - 更新用户信息
router.put('/:id',
  param('id').isUUID(),
  body('firstName').optional().trim().isLength({ min: 1, max: 50 }),
  body('lastName').optional().trim().isLength({ min: 1, max: 50 }),
  validate,
  userController.updateUser
);

// DELETE /api/users/:id - 删除用户(仅组织管理员可用)
router.delete('/:id',
  requireRole(['org_admin']),
  param('id').isUUID(),
  validate,
  userController.deleteUser
);

export default router;

Controller Pattern

控制器模式

javascript
// services/user-service/src/controllers/user.controller.js
import { UserModel } from '../models/User.js';
import { KeycloakService } from '../services/keycloak.service.js';
import { AppError } from '../utils/AppError.js';

export async function listUsers(req, res, next) {
  try {
    const { org_id, page = 1, limit = 20 } = req.query;

    // Ensure user can only list users from their org (unless super admin)
    const orgIdFilter = req.user.roles.includes('super_admin')
      ? org_id
      : req.user.org_id;

    if (!orgIdFilter) {
      throw new AppError('Organization ID required', 400);
    }

    const users = await UserModel.find({ org_id: orgIdFilter })
      .select('-password')
      .limit(limit)
      .skip((page - 1) * limit)
      .sort({ createdAt: -1 });

    const total = await UserModel.countDocuments({ org_id: orgIdFilter });

    res.json({
      users,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total,
        pages: Math.ceil(total / limit)
      }
    });
  } catch (error) {
    next(error);
  }
}

export async function getUser(req, res, next) {
  try {
    const { id } = req.params;

    const user = await UserModel.findById(id).select('-password');

    if (!user) {
      throw new AppError('User not found', 404);
    }

    // Ensure user can only access users from their org
    if (user.org_id !== req.user.org_id && !req.user.roles.includes('super_admin')) {
      throw new AppError('Access denied', 403);
    }

    res.json(user);
  } catch (error) {
    next(error);
  }
}

export async function createUser(req, res, next) {
  try {
    const { email, firstName, lastName, orgId } = req.body;

    // Verify org_id matches user's org (unless super admin)
    if (orgId !== req.user.org_id && !req.user.roles.includes('super_admin')) {
      throw new AppError('Cannot create user for different organization', 403);
    }

    // Create user in Keycloak
    const keycloakService = new KeycloakService();
    const keycloakUserId = await keycloakService.createUser({
      email,
      firstName,
      lastName,
      orgId
    });

    // Create user in MongoDB
    const user = new UserModel({
      keycloakId: keycloakUserId,
      email,
      firstName,
      lastName,
      org_id: orgId,
      createdBy: req.user.sub
    });

    await user.save();

    res.status(201).json({
      id: user._id,
      keycloakId: keycloakUserId,
      email: user.email
    });
  } catch (error) {
    next(error);
  }
}
javascript
// services/user-service/src/controllers/user.controller.js
import { UserModel } from '../models/User.js';
import { KeycloakService } from '../services/keycloak.service.js';
import { AppError } from '../utils/AppError.js';

export async function listUsers(req, res, next) {
  try {
    const { org_id, page = 1, limit = 20 } = req.query;

    // 确保用户只能查看自己组织的用户(超级管理员除外)
    const orgIdFilter = req.user.roles.includes('super_admin')
      ? org_id
      : req.user.org_id;

    if (!orgIdFilter) {
      throw new AppError('需要组织ID', 400);
    }

    const users = await UserModel.find({ org_id: orgIdFilter })
      .select('-password')
      .limit(limit)
      .skip((page - 1) * limit)
      .sort({ createdAt: -1 });

    const total = await UserModel.countDocuments({ org_id: orgIdFilter });

    res.json({
      users,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total,
        pages: Math.ceil(total / limit)
      }
    });
  } catch (error) {
    next(error);
  }
}

export async function getUser(req, res, next) {
  try {
    const { id } = req.params;

    const user = await UserModel.findById(id).select('-password');

    if (!user) {
      throw new AppError('用户未找到', 404);
    }

    // 确保用户只能访问自己组织的用户信息
    if (user.org_id !== req.user.org_id && !req.user.roles.includes('super_admin')) {
      throw new AppError('访问被拒绝', 403);
    }

    res.json(user);
  } catch (error) {
    next(error);
  }
}

export async function createUser(req, res, next) {
  try {
    const { email, firstName, lastName, orgId } = req.body;

    // 验证组织ID与用户所属组织一致(超级管理员除外)
    if (orgId !== req.user.org_id && !req.user.roles.includes('super_admin')) {
      throw new AppError('无法为其他组织创建用户', 403);
    }

    // 在Keycloak中创建用户
    const keycloakService = new KeycloakService();
    const keycloakUserId = await keycloakService.createUser({
      email,
      firstName,
      lastName,
      orgId
    });

    // 在MongoDB中创建用户
    const user = new UserModel({
      keycloakId: keycloakUserId,
      email,
      firstName,
      lastName,
      org_id: orgId,
      createdBy: req.user.sub
    });

    await user.save();

    res.status(201).json({
      id: user._id,
      keycloakId: keycloakUserId,
      email: user.email
    });
  } catch (error) {
    next(error);
  }
}

MongoDB Schema Patterns

MongoDB Schema模式

User Model

用户模型

javascript
// services/user-service/src/models/User.js
import mongoose from 'mongoose';

const userSchema = new mongoose.Schema({
  keycloakId: {
    type: String,
    required: true,
    unique: true,
    index: true
  },
  email: {
    type: String,
    required: true,
    lowercase: true,
    trim: true,
    index: true
  },
  firstName: {
    type: String,
    required: true,
    trim: true
  },
  lastName: {
    type: String,
    required: true,
    trim: true
  },
  org_id: {
    type: String,
    required: true,
    index: true
  },
  roles: [{
    type: String,
    enum: ['org_admin', 'org_user', 'super_admin']
  }],
  metadata: {
    type: Map,
    of: String
  },
  createdBy: String,
  updatedBy: String
}, {
  timestamps: true,
  toJSON: {
    transform: (doc, ret) => {
      ret.id = ret._id;
      delete ret._id;
      delete ret.__v;
      return ret;
    }
  }
});

// Compound index for org queries
userSchema.index({ org_id: 1, email: 1 }, { unique: true });

// Virtual for full name
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

// Pre-save hook
userSchema.pre('save', function(next) {
  if (this.isModified('email')) {
    this.email = this.email.toLowerCase();
  }
  next();
});

export const UserModel = mongoose.model('User', userSchema);
javascript
// services/user-service/src/models/User.js
import mongoose from 'mongoose';

const userSchema = new mongoose.Schema({
  keycloakId: {
    type: String,
    required: true,
    unique: true,
    index: true
  },
  email: {
    type: String,
    required: true,
    lowercase: true,
    trim: true,
    index: true
  },
  firstName: {
    type: String,
    required: true,
    trim: true
  },
  lastName: {
    type: String,
    required: true,
    trim: true
  },
  org_id: {
    type: String,
    required: true,
    index: true
  },
  roles: [{
    type: String,
    enum: ['org_admin', 'org_user', 'super_admin']
  }],
  metadata: {
    type: Map,
    of: String
  },
  createdBy: String,
  updatedBy: String
}, {
  timestamps: true,
  toJSON: {
    transform: (doc, ret) => {
      ret.id = ret._id;
      delete ret._id;
      delete ret.__v;
      return ret;
    }
  }
});

// 组织查询的复合索引
userSchema.index({ org_id: 1, email: 1 }, { unique: true });

// 全名虚拟字段
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

// 预保存钩子
userSchema.pre('save', function(next) {
  if (this.isModified('email')) {
    this.email = this.email.toLowerCase();
  }
  next();
});

export const UserModel = mongoose.model('User', userSchema);

Organization Model

组织模型

javascript
// services/org-service/src/models/Organization.js
import mongoose from 'mongoose';

const organizationSchema = new mongoose.Schema({
  org_id: {
    type: String,
    required: true,
    unique: true,
    index: true
  },
  name: {
    type: String,
    required: true,
    trim: true
  },
  domain: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  settings: {
    theme: {
      type: String,
      default: 'lobbi-base'
    },
    features: {
      type: Map,
      of: Boolean,
      default: new Map()
    },
    branding: {
      logoUrl: String,
      primaryColor: String,
      secondaryColor: String
    }
  },
  subscription: {
    plan: {
      type: String,
      enum: ['free', 'starter', 'professional', 'enterprise'],
      default: 'free'
    },
    status: {
      type: String,
      enum: ['active', 'inactive', 'suspended'],
      default: 'active'
    },
    billingCycle: {
      type: String,
      enum: ['monthly', 'annual']
    },
    stripeCustomerId: String,
    stripeSubscriptionId: String
  },
  adminUsers: [{
    userId: String,
    email: String,
    addedAt: Date
  }],
  status: {
    type: String,
    enum: ['active', 'inactive', 'suspended'],
    default: 'active'
  }
}, {
  timestamps: true
});

export const OrganizationModel = mongoose.model('Organization', organizationSchema);
javascript
// services/org-service/src/models/Organization.js
import mongoose from 'mongoose';

const organizationSchema = new mongoose.Schema({
  org_id: {
    type: String,
    required: true,
    unique: true,
    index: true
  },
  name: {
    type: String,
    required: true,
    trim: true
  },
  domain: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  settings: {
    theme: {
      type: String,
      default: 'lobbi-base'
    },
    features: {
      type: Map,
      of: Boolean,
      default: new Map()
    },
    branding: {
      logoUrl: String,
      primaryColor: String,
      secondaryColor: String
    }
  },
  subscription: {
    plan: {
      type: String,
      enum: ['free', 'starter', 'professional', 'enterprise'],
      default: 'free'
    },
    status: {
      type: String,
      enum: ['active', 'inactive', 'suspended'],
      default: 'active'
    },
    billingCycle: {
      type: String,
      enum: ['monthly', 'annual']
    },
    stripeCustomerId: String,
    stripeSubscriptionId: String
  },
  adminUsers: [{
    userId: String,
    email: String,
    addedAt: Date
  }],
  status: {
    type: String,
    enum: ['active', 'inactive', 'suspended'],
    default: 'active'
  }
}, {
  timestamps: true
});

export const OrganizationModel = mongoose.model('Organization', organizationSchema);

API Gateway Architecture

API网关架构

Gateway Setup

网关配置

javascript
// services/api-gateway/src/index.js
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { authMiddleware } from './middleware/auth.js';
import { rateLimiter } from './middleware/rateLimit.js';
import { cacheMiddleware } from './middleware/cache.js';

const app = express();

// Rate limiting
app.use(rateLimiter);

// Authentication
app.use(authMiddleware);

// Service routing
const services = {
  users: process.env.USER_SERVICE_URL || 'http://localhost:5001',
  orgs: process.env.ORG_SERVICE_URL || 'http://localhost:5002',
  tenants: process.env.TENANT_SERVICE_URL || 'http://localhost:5003',
  notifications: process.env.NOTIFICATION_SERVICE_URL || 'http://localhost:5004',
  billing: process.env.BILLING_SERVICE_URL || 'http://localhost:5005',
  analytics: process.env.ANALYTICS_SERVICE_URL || 'http://localhost:5006'
};

// Proxy to microservices
Object.entries(services).forEach(([name, target]) => {
  app.use(`/api/${name}`, createProxyMiddleware({
    target,
    changeOrigin: true,
    pathRewrite: { [`^/api/${name}`]: '' },
    onProxyReq: (proxyReq, req) => {
      // Forward user context
      if (req.user) {
        proxyReq.setHeader('X-User-Id', req.user.sub);
        proxyReq.setHeader('X-Org-Id', req.user.org_id);
        proxyReq.setHeader('X-User-Roles', JSON.stringify(req.user.roles));
      }
    }
  }));
});

app.listen(4000, () => {
  console.log('API Gateway running on port 4000');
});
javascript
// services/api-gateway/src/index.js
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { authMiddleware } from './middleware/auth.js';
import { rateLimiter } from './middleware/rateLimit.js';
import { cacheMiddleware } from './middleware/cache.js';

const app = express();

// 限流
app.use(rateLimiter);

// 认证
app.use(authMiddleware);

// 服务路由
const services = {
  users: process.env.USER_SERVICE_URL || 'http://localhost:5001',
  orgs: process.env.ORG_SERVICE_URL || 'http://localhost:5002',
  tenants: process.env.TENANT_SERVICE_URL || 'http://localhost:5003',
  notifications: process.env.NOTIFICATION_SERVICE_URL || 'http://localhost:5004',
  billing: process.env.BILLING_SERVICE_URL || 'http://localhost:5005',
  analytics: process.env.ANALYTICS_SERVICE_URL || 'http://localhost:5006'
};

// 代理到微服务
Object.entries(services).forEach(([name, target]) => {
  app.use(`/api/${name}`, createProxyMiddleware({
    target,
    changeOrigin: true,
    pathRewrite: { [`^/api/${name}`]: '' },
    onProxyReq: (proxyReq, req) => {
      // 转发用户上下文
      if (req.user) {
        proxyReq.setHeader('X-User-Id', req.user.sub);
        proxyReq.setHeader('X-Org-Id', req.user.org_id);
        proxyReq.setHeader('X-User-Roles', JSON.stringify(req.user.roles));
      }
    }
  }));
});

app.listen(4000, () => {
  console.log('API网关运行在端口4000');
});

Session and Cookie Management

会话与Cookie管理

Session Configuration

会话配置

javascript
// services/api-gateway/src/config/session.js
import session from 'express-session';
import MongoStore from 'connect-mongo';

export const sessionConfig = session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,

  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URL,
    ttl: 24 * 60 * 60, // 1 day
    touchAfter: 24 * 3600 // lazy session update
  }),

  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24, // 24 hours
    sameSite: 'lax',
    domain: process.env.COOKIE_DOMAIN
  },

  name: 'lobbi.sid'
});
javascript
// services/api-gateway/src/config/session.js
import session from 'express-session';
import MongoStore from 'connect-mongo';

export const sessionConfig = session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,

  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URL,
    ttl: 24 * 60 * 60, // 1天
    touchAfter: 24 * 3600 // 延迟会话更新
  }),

  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24, // 24小时
    sameSite: 'lax',
    domain: process.env.COOKIE_DOMAIN
  },

  name: 'lobbi.sid'
});

Error Handling Patterns

错误处理模式

Custom Error Classes

自定义错误类

javascript
// shared/utils/AppError.js
export class AppError extends Error {
  constructor(message, statusCode = 500, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class ValidationError extends AppError {
  constructor(message, errors = []) {
    super(message, 400);
    this.errors = errors;
  }
}

export class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, 404);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401);
  }
}

export class ForbiddenError extends AppError {
  constructor(message = 'Forbidden') {
    super(message, 403);
  }
}
javascript
// shared/utils/AppError.js
export class AppError extends Error {
  constructor(message, statusCode = 500, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class ValidationError extends AppError {
  constructor(message, errors = []) {
    super(message, 400);
    this.errors = errors;
  }
}

export class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource}未找到`, 404);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = '未授权') {
    super(message, 401);
  }
}

export class ForbiddenError extends AppError {
  constructor(message = '禁止访问') {
    super(message, 403);
  }
}

Error Handler Middleware

错误处理中间件

javascript
// services/user-service/src/middleware/errorHandler.js
import { AppError } from '../utils/AppError.js';

export function errorHandler(err, req, res, next) {
  let { statusCode, message, isOperational } = err;

  // Default to 500 server error
  if (!statusCode) {
    statusCode = 500;
    isOperational = false;
  }

  // Log error
  console.error('Error:', {
    message,
    statusCode,
    isOperational,
    stack: err.stack,
    url: req.url,
    method: req.method,
    user: req.user?.sub
  });

  // Send error response
  res.status(statusCode).json({
    error: {
      message: isOperational ? message : 'Internal server error',
      statusCode,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
}

// Async error wrapper
export function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}
javascript
// services/user-service/src/middleware/errorHandler.js
import { AppError } from '../utils/AppError.js';

export function errorHandler(err, req, res, next) {
  let { statusCode, message, isOperational } = err;

  // 默认500服务器错误
  if (!statusCode) {
    statusCode = 500;
    isOperational = false;
  }

  // 记录错误
  console.error('错误:', {
    message,
    statusCode,
    isOperational,
    stack: err.stack,
    url: req.url,
    method: req.method,
    user: req.user?.sub
  });

  // 返回错误响应
  res.status(statusCode).json({
    error: {
      message: isOperational ? message : '内部服务器错误',
      statusCode,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
}

// 异步错误包装器
export function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

Testing Strategies

测试策略

Unit Tests with Jest

Jest单元测试

javascript
// services/user-service/tests/controllers/user.controller.test.js
import { listUsers, createUser } from '../../src/controllers/user.controller.js';
import { UserModel } from '../../src/models/User.js';
import { KeycloakService } from '../../src/services/keycloak.service.js';

jest.mock('../../src/models/User.js');
jest.mock('../../src/services/keycloak.service.js');

describe('UserController', () => {
  describe('listUsers', () => {
    it('should return paginated users for org', async () => {
      const mockUsers = [
        { _id: '1', email: 'user1@test.com', org_id: 'org_1' },
        { _id: '2', email: 'user2@test.com', org_id: 'org_1' }
      ];

      UserModel.find.mockReturnValue({
        select: jest.fn().mockReturnThis(),
        limit: jest.fn().mockReturnThis(),
        skip: jest.fn().mockReturnThis(),
        sort: jest.fn().mockResolvedValue(mockUsers)
      });

      UserModel.countDocuments.mockResolvedValue(2);

      const req = {
        query: { org_id: 'org_1', page: 1, limit: 20 },
        user: { org_id: 'org_1', roles: ['org_admin'] }
      };
      const res = {
        json: jest.fn()
      };
      const next = jest.fn();

      await listUsers(req, res, next);

      expect(res.json).toHaveBeenCalledWith({
        users: mockUsers,
        pagination: expect.objectContaining({
          page: 1,
          total: 2
        })
      });
    });
  });
});
javascript
// services/user-service/tests/controllers/user.controller.test.js
import { listUsers, createUser } from '../../src/controllers/user.controller.js';
import { UserModel } from '../../src/models/User.js';
import { KeycloakService } from '../../src/services/keycloak.service.js';

jest.mock('../../src/models/User.js');
jest.mock('../../src/services/keycloak.service.js');

describe('UserController', () => {
  describe('listUsers', () => {
    it('应返回组织的分页用户列表', async () => {
      const mockUsers = [
        { _id: '1', email: 'user1@test.com', org_id: 'org_1' },
        { _id: '2', email: 'user2@test.com', org_id: 'org_1' }
      ];

      UserModel.find.mockReturnValue({
        select: jest.fn().mockReturnThis(),
        limit: jest.fn().mockReturnThis(),
        skip: jest.fn().mockReturnThis(),
        sort: jest.fn().mockResolvedValue(mockUsers)
      });

      UserModel.countDocuments.mockResolvedValue(2);

      const req = {
        query: { org_id: 'org_1', page: 1, limit: 20 },
        user: { org_id: 'org_1', roles: ['org_admin'] }
      };
      const res = {
        json: jest.fn()
      };
      const next = jest.fn();

      await listUsers(req, res, next);

      expect(res.json).toHaveBeenCalledWith({
        users: mockUsers,
        pagination: expect.objectContaining({
          page: 1,
          total: 2
        })
      });
    });
  });
});

Integration Tests

集成测试

javascript
// services/user-service/tests/integration/users.test.js
import request from 'supertest';
import { app } from '../../src/index.js';
import { connectDB, closeDB, clearDB } from '../setup.js';

beforeAll(async () => await connectDB());
afterEach(async () => await clearDB());
afterAll(async () => await closeDB());

describe('User API Integration', () => {
  it('POST /api/users - should create user', async () => {
    const userData = {
      email: 'test@example.com',
      firstName: 'Test',
      lastName: 'User',
      orgId: 'org_test'
    };

    const response = await request(app)
      .post('/api/users')
      .set('Authorization', `Bearer ${mockAdminToken}`)
      .send(userData)
      .expect(201);

    expect(response.body).toMatchObject({
      email: userData.email
    });
  });
});
javascript
// services/user-service/tests/integration/users.test.js
import request from 'supertest';
import { app } from '../../src/index.js';
import { connectDB, closeDB, clearDB } from '../setup.js';

beforeAll(async () => await connectDB());
afterEach(async () => await clearDB());
afterAll(async () => await closeDB());

describe('用户API集成测试', () => {
  it('POST /api/users - 应创建用户', async () => {
    const userData = {
      email: 'test@example.com',
      firstName: 'Test',
      lastName: 'User',
      orgId: 'org_test'
    };

    const response = await request(app)
      .post('/api/users')
      .set('Authorization', `Bearer ${mockAdminToken}`)
      .send(userData)
      .expect(201);

    expect(response.body).toMatchObject({
      email: userData.email
    });
  });
});

Best Practices

最佳实践

  1. Use environment variables for all configuration
  2. Implement proper error handling with custom error classes
  3. Validate all inputs using express-validator or Joi
  4. Use async/await consistently, avoid callback hell
  5. Implement proper logging with structured logs
  6. Use MongoDB indexes for frequently queried fields
  7. Implement rate limiting to prevent abuse
  8. Use CORS properly with specific origins
  9. Implement request/response compression with gzip
  10. Use TypeScript for better type safety (optional)
  11. Implement health checks for all services
  12. Use connection pooling for database connections
  13. Implement graceful shutdown for services
  14. Use dependency injection for better testability
  15. Implement proper security headers with Helmet
  1. 使用环境变量管理所有配置项
  2. 实现完善的错误处理,使用自定义错误类
  3. 验证所有输入,使用express-validator或Joi
  4. 统一使用async/await,避免回调地狱
  5. 实现结构化日志记录
  6. 为MongoDB频繁查询的字段添加索引
  7. 实现限流机制,防止服务被滥用
  8. 合理配置CORS,指定允许的来源
  9. 使用gzip实现请求/响应压缩
  10. 使用TypeScript提升类型安全性(可选)
  11. 为所有服务添加健康检查接口
  12. 使用数据库连接池
  13. 实现服务优雅关闭
  14. 使用依赖注入提升可测试性
  15. 使用Helmet添加安全响应头

File Locations in keycloak-alpha

keycloak-alpha文件位置说明

PathPurpose
apps/web-app/
React + Vite main application
services/api-gateway/
API Gateway with routing
services/user-service/
User management microservice
services/org-service/
Organization management
routes/api/
Shared route definitions
shared/utils/
Shared utilities and helpers
路径用途
apps/web-app/
React + Vite主应用
services/api-gateway/
API网关与路由管理
services/user-service/
用户管理微服务
services/org-service/
组织管理服务
routes/api/
共享路由定义
shared/utils/
共享工具与辅助函数