feat(console): add ASP.NET Core Identity dependency

This commit is contained in:
Sam 2026-02-05 14:06:23 +08:00
parent 29357dbf10
commit b8a76dfd93
180 changed files with 25 additions and 28187 deletions

View File

@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.5",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}

130
.gitignore vendored
View File

@ -1,130 +0,0 @@
## Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleasePublic/
x64/
x86/
[Aa][Rr][Ii][Ll][Mm][Oo][Uu][Tt][Ee][Rr]/[Aa][Pp][Pp][Bb][Uu][Ii][Ll][Dd]/
x64/
x86/
[Aa]rm/
[Aa]rm64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
## Visual Studio cache/options directory
.vs/
.vscode/
## Rider
.idea/
## Git Worktrees
.worktrees/
## User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
## Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleasePublic/
x64/
x86/
[Aa][Rr][Ii][Ll][Mm][Oo][Uu][Tt][Ee][Rr]/
[Aa][Rr][Ii][Ll][Mm][Ee][Dd]/
[Aa][Nn][Uu][Tt][Tt][Ee][Rr][Dd][Ee][Rr]/
## NuGet Packages
*.nupkg
*.snupkg
**/packages/*
!**/packages/build/
## .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
## ASP.NET Scaffolding
ScaffoldingReadMe.txt
## Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
## Test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
## Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
## Vue / Vite
dist/
dist-ssr/
*.local
.nuxt/
.vuepress/dist/
## Environment
.env
.env.local
.env.*.local
## OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
## Database
*.db
*.sqlite
*.sqlite3
## Docker
*.log
docker-compose.override.yml

View File

@ -1,3 +0,0 @@
{
"deepscan.enable": true
}

View File

@ -1,316 +0,0 @@
# 风铃认证中心 - 管理端开发完成
## 项目概述
本项目为风铃认证中心提供完整的 Web 管理界面和后端 API。
## 技术栈
### 前端
- Vue 3 + TypeScript
- Vite
- Element Plus UI 框架
- Pinia 状态管理
- Vue Router 路由
- Axios HTTP 客户端
### 后端
- ASP.NET Core 10.0
- Entity Framework Core 10.0
- PostgreSQL 数据库
- OpenIddict OAuth2/OIDC 服务
- Serilog 日志
- OpenTelemetry 可观测性
## 完成的功能
### 前端页面
#### 1. 仪表盘Dashboard
- 统计卡片用户数、租户数、OAuth应用数、今日访问
- 最近活动时间线
- 系统信息展示
- 位置:`src/views/Dashboard/Dashboard.vue`
#### 2. 用户管理
- 用户列表(分页、搜索)
- 添加/编辑用户
- 角色分配
- 重置密码
- 删除用户
- 位置:`src/views/Users/UserList.vue`
#### 3. 角色管理
- 角色列表(分页、搜索)
- 添加/编辑角色
- 权限配置
- 查看角色用户
- 移除用户角色
- 位置:`src/views/Users/RoleList.vue`
#### 4. 租户管理
- 租户列表(分页、搜索)
- 添加/编辑租户
- 租户设置(注册限制、密码策略、会话超时)
- 查看租户用户和角色
- 位置:`src/views/Users/TenantList.vue`
#### 5. OAuth 应用管理
- 应用列表(分页、搜索)
- 添加/编辑应用
- 完整配置重定向URI、授权类型、权限范围等
- 查看密钥
- 位置:`src/views/OAuth/ClientList.vue`
#### 6. 访问日志
- 日志列表(多条件筛选)
- 日志详情查看
- 导出 CSV
- 位置:`src/views/Audit/AccessLog.vue`
#### 7. 审计日志
- 日志列表(多条件筛选)
- 日志详情查看(包含变更前/后数据)
- 导出 CSV
- 位置:`src/views/Audit/AuditLog.vue`
### 后端 API
#### 控制器
1. **AuthController** - 认证端点
- POST /connect/token - 登录(密码模式)
- POST /connect/token/refresh - 刷新令牌
- POST /connect/revoke - 撤销令牌
2. **UsersController** - 用户管理
- GET /api/users - 获取用户列表(分页、搜索)
- GET /api/users/{id} - 获取单个用户
- POST /api/users - 创建用户
- PUT /api/users/{id} - 更新用户
- PUT /api/users/{id}/password - 重置密码
- DELETE /api/users/{id} - 删除用户
3. **RolesController** - 角色管理
- GET /api/roles - 获取角色列表(分页、搜索)
- GET /api/roles/{id} - 获取单个角色
- GET /api/roles/{id}/users - 获取角色用户
- POST /api/roles - 创建角色
- PUT /api/roles/{id} - 更新角色
- DELETE /api/roles/{id} - 删除角色
- DELETE /api/roles/{id}/users/{userId} - 移除用户角色
4. **TenantsController** - 租户管理
- GET /api/tenants - 获取租户列表(分页、搜索)
- GET /api/tenants/{id} - 获取单个租户
- GET /api/tenants/{tenantId}/users - 获取租户用户
- GET /api/tenants/{tenantId}/roles - 获取租户角色
- GET /api/tenants/{tenantId}/settings - 获取租户设置
- PUT /api/tenants/{tenantId}/settings - 更新租户设置
- POST /api/tenants - 创建租户
- PUT /api/tenants/{id} - 更新租户
- DELETE /api/tenants/{id} - 删除租户
5. **OAuthClientsController** - OAuth 应用管理
- GET /api/oauthclients - 获取应用列表(分页、搜索)
- GET /api/oauthclients/{id} - 获取单个应用
- GET /api/oauthclients/{id}/secret - 获取应用密钥
- POST /api/oauthclients - 创建应用
- PUT /api/oauthclients/{id} - 更新应用
- DELETE /api/oauthclients/{id} - 删除应用
6. **AccessLogsController** - 访问日志
- GET /api/access-logs - 获取日志列表(分页、筛选)
- GET /api/access-logs/export - 导出 CSV
7. **AuditLogsController** - 审计日志
- GET /api/audit-logs - 获取日志列表(分页、筛选)
- GET /api/audit-logs/export - 导出 CSV
8. **StatsController** - 统计数据
- GET /api/stats/dashboard - 仪表盘统计数据
- GET /api/stats/system - 系统统计信息
9. **HealthCheckController** - 健康检查
- GET /health - 健康检查端点
### 数据模型
#### 核心模型
1. **ApplicationUser** - 用户
- 继承自 IdentityUser<long>
- RealName, Phone, TenantId, CreatedTime, UpdatedTime, IsDeleted
2. **ApplicationRole** - 角色
- 继承自 IdentityRole<long>
- Description, DisplayName, TenantId, IsSystem, Permissions, CreatedTime
3. **Tenant** - 租户
- Id, TenantId, Name, ContactName, ContactEmail, ContactPhone
- MaxUsers, Description, Status, ExpiresAt, CreatedAt, UpdatedAt, IsDeleted
4. **OAuthApplication** - OAuth 应用
- Id, ClientId, ClientSecret, DisplayName
- RedirectUris, PostLogoutRedirectUris, Scopes, GrantTypes
- ClientType, ConsentType, Status, Description, CreatedAt, UpdatedAt
5. **AccessLog** - 访问日志
- UserName, TenantId, Action, Resource, Method, IpAddress
- UserAgent, Status, Duration, RequestData, ResponseData, ErrorMessage
- CreatedAt
6. **AuditLog** - 审计日志
- Operator, TenantId, Operation, Action
- TargetType, TargetId, TargetName, IpAddress, Description
- OldValue, NewValue, ErrorMessage, Status, CreatedAt
### 数据库配置
- **数据库类型**: PostgreSQL
- **连接字符串**: `Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542`
- **迁移**:
- InitialCreate - 初始化表结构
- AddOAuthApplications - 添加 OAuth 应用表
- AddTenantAndLogs - 添加租户和日志表
- AddOAuthDescription - 添加 OAuth 描述字段
### 初始数据
SeedData 初始化以下数据:
#### 默认租户
- TenantId: default
- Name: 默认租户
- MaxUsers: 1000
#### 系统角色
1. **Admin** - 管理员
- 所有权限
- IsSystem: true
2. **User** - 普通用户
- user.view 权限
- IsSystem: true
#### 默认用户
1. **admin**
- 密码: Admin@123
- 角色: Admin
2. **testuser**
- 密码: Test@123
- 角色: User
#### OAuth 应用
- **fengling-console** - 风铃运管中心
- ClientSecret: console-secret-change-in-production
- 授权类型: authorization_code, refresh_token
## 运行说明
### 后端AuthService
```bash
cd src/Fengling.AuthService
dotnet build
dotnet run
```
服务地址: http://localhost:5000
API 文档: http://localhost:5000/swagger
健康检查: http://localhost:5000/health
### 前端Console.Web
```bash
cd src/Fengling.Console.Web
npm install
npm run dev
```
开发地址: http://localhost:5173
生产构建:
```bash
npm run build
```
## API 代理配置
Vite 开发服务器配置了 API 代理:
- `/api/auth/*``http://localhost:5000` (AuthService)
- `/api/gateway/*``http://localhost:5001` (YarpGateway)
## 默认登录
- 用户名: admin
- 密码: Admin@123
## 文件结构
```
Fengling.Refactory.Buiding/
├── src/
│ ├── Fengling.AuthService/ # 后端认证服务
│ │ ├── Controllers/ # API 控制器
│ │ ├── Data/ # 数据库上下文和种子数据
│ │ ├── Models/ # 数据模型
│ │ ├── Configuration/ # 配置类
│ │ └── Migrations/ # 数据库迁移
│ │
│ └── Fengling.Console.Web/ # 前端管理界面
│ ├── src/
│ │ ├── api/ # API 调用封装
│ │ ├── stores/ # Pinia 状态管理
│ │ ├── router/ # Vue Router 路由
│ │ └── views/ # 页面组件
│ │ ├── Auth/ # 认证页面
│ │ ├── Dashboard/ # 仪表盘
│ │ ├── Users/ # 用户/角色/租户管理
│ │ ├── OAuth/ # OAuth 应用管理
│ │ └── Audit/ # 日志管理
│ └── dist/ # 生产构建输出
└── docs/ # 项目文档
```
## 安全建议
1. **生产环境修改**:
- 更改默认管理员密码
- 修改 OAuth 应用密钥
- 配置 HTTPS
- 限制数据库访问
2. **密码策略**:
- 至少 8 位
- 包含字母和数字
- 可根据租户设置自定义策略
## 后续优化建议
1. **前端**:
- 添加更多图表可视化
- 实现前端国际化
- 添加单元测试
2. **后端**:
- 添加 API 限流
- 实现缓存策略
- 添加更多日志级别
- 完善错误处理
3. **功能**:
- OAuth 授权码流程完善
- 双因素认证2FA
- 用户自助服务(找回密码、注册)
- API 权限细粒度控制
## 版本信息
- 前端版本: v1.0.0
- 后端版本: v1.0.0
- .NET 版本: 10.0.2
- Node.js 版本: 推荐 18+

25
Fengling.Console.csproj Normal file
View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="OpenIddict.Abstractions" Version="7.2.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="7.2.0" />
<PackageReference Include="OpenIddict.Server" Version="7.2.0" />
<PackageReference Include="OpenIddict.Server.AspNetCore" Version="7.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fengling.AuthService\Fengling.AuthService.csproj" />
</ItemGroup>
</Project>

View File

@ -1,54 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YarpGateway", "src\YarpGateway\YarpGateway.csproj", "{8DDFE39A-06AE-4C02-BA80-27F0C809E959}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fengling.AuthService", "src\Fengling.AuthService\Fengling.AuthService.csproj", "{469FA168-1656-483D-A40D-072FFE8C5E33}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Debug|x64.ActiveCfg = Debug|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Debug|x64.Build.0 = Debug|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Debug|x86.ActiveCfg = Debug|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Debug|x86.Build.0 = Debug|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Release|Any CPU.Build.0 = Release|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Release|x64.ActiveCfg = Release|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Release|x64.Build.0 = Release|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Release|x86.ActiveCfg = Release|Any CPU
{8DDFE39A-06AE-4C02-BA80-27F0C809E959}.Release|x86.Build.0 = Release|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Debug|x64.ActiveCfg = Debug|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Debug|x64.Build.0 = Debug|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Debug|x86.ActiveCfg = Debug|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Debug|x86.Build.0 = Debug|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Release|Any CPU.Build.0 = Release|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Release|x64.ActiveCfg = Release|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Release|x64.Build.0 = Release|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Release|x86.ActiveCfg = Release|Any CPU
{469FA168-1656-483D-A40D-072FFE8C5E33}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8DDFE39A-06AE-4C02-BA80-27F0C809E959} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{469FA168-1656-483D-A40D-072FFE8C5E33} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal

View File

@ -1,72 +0,0 @@
# 风铃认证中心 - 布局和路由修复
## 修复的问题
1. **菜单点击不跳转** - 添加了 `router.push({ name: index })``handleMenuSelect`
2. **App.vue 缺少 router-view** - 更新为包含 `<router-view />` 和基础样式
3. **面包屑显示** - 优化为只在非 Dashboard 页面显示
4. **内容区域样式** - 添加 `content-wrapper` 包裹,优化布局
## 现在的布局结构
```
App.vue
└── <router-view />
├── Login.vue (路径: /login)
├── Callback.vue (路径: /auth/callback)
└── Dashboard.vue (路径: / 及其子路由)
├── 侧边栏 (el-aside)
└── 主内容区 (el-main)
├── 面包屑 (可选)
└── <router-view /> (嵌套子路由)
├── Dashboard/Dashboard.vue
├── Users/UserList.vue
├── Users/RoleList.vue
├── Users/TenantList.vue
├── OAuth/ClientList.vue
├── Audit/AccessLog.vue
└── Audit/AuditLog.vue
```
## 运行项目
### 启动后端
```bash
cd src/Fengling.AuthService
dotnet run
```
### 启动前端
```bash
cd src/Fengling.Console.Web
npm run dev
```
访问: http://localhost:5173
默认登录:
- 用户名: admin
- 密码: Admin@123
## 功能测试清单
- [ ] 登录页面正常显示
- [ ] 登录成功后跳转到仪表盘
- [ ] 侧边栏菜单正常显示
- [ ] 点击菜单项正确跳转
- [ ] 当前菜单项高亮显示
- [ ] 面包屑导航正确显示
- [ ] 各个管理页面正常加载
- [ ] 退出登录功能正常
## 页面路由
| 路径 | 名称 | 页面 |
|------|------|------|
| / | Dashboard | 仪表盘 |
| /users | UserList | 用户列表 |
| /roles | RoleList | 角色管理 |
| /tenants | TenantList | 租户管理 |
| /oauth/clients | OAuthClients | OAuth 应用 |
| /logs/access | AccessLog | 访问日志 |
| /logs/audit | AuditLog | 审计日志 |

View File

@ -1,288 +0,0 @@
---
# Fengling Project Rewrite - Conversation Summary
## Project Information
**Project Name**: Fengling (风灵) - QR Code Marketing Management Platform Rewrite
**Project Locations**:
- **Old Project**: `/Users/movingsam/Fengling.Refactory/`
- `Fengling.Backend.Web/` - Old monolithic backend
- `Yarp.Gateway/` - Old gateway (no longer relevant for reference)
- **New Project**: `/Users/movingsam/Fengling.Refactory.Buiding/`
- `src/YarpGateway/` - New independent gateway service
- `src/YarpGateway.Admin/` - Vue3 admin UI
## What We Did
### Phase 1: Initial Gateway Setup
1. Created YARP Gateway backend with:
- YARP 2.2.0 (reverse proxy framework)
- EF Core 9.0.0 + PostgreSQL (192.168.100.10:5432)
- StackExchange.Redis (192.168.100.10:6379) for distributed locks
- Serilog for logging
2. Created Vue3 Admin frontend with:
- Vue 3 + TypeScript + Vite + Element Plus + Pinia
- Running on http://localhost:5173
### Phase 2: Core Architecture Implementation
#### 1. Route Priority Design (99% Global + 1% Tenant-Specific)
**User's requirement**:
> "普通情况租户走特定的实例pod只是特殊场景所以是要对 前缀匹配到不同的服务这块的ui呢"
**Implementation**:
- Added `IsGlobal` boolean field to `GwTenantRoute` table
- Route priority: Tenant-specific routes > Global routes
- Database migration: `20260201133826_AddIsGlobalToTenantRoute`
**Benefit**:
- Before: 100 tenants × 10 services = 1000 route configurations
- After: 10 global routes + few tenant-specific routes
#### 2. In-Memory Route Caching
**File**: `src/YarpGateway/Services/RouteCache.cs`
- Loads routes from database at startup
- Priority-based lookup: tenant route → global route → 404
- Hot reload support via `ReloadAsync()`
- Avoids database queries per request
#### 3. Redis Distributed Load Balancing
**File**: `src/YarpGateway/LoadBalancing/DistributedWeightedRoundRobinPolicy.cs`
- Implements weighted round-robin algorithm
- Uses Redis for distributed locks: `lock:{instanceName}:{clusterId}`
- Stores load balancing state in Redis: `lb:{instanceName}:{clusterId}:state`
- Supports multiple gateway instances
#### 4. Dynamic Proxy Configuration
**File**: `src/YarpGateway/DynamicProxy/DynamicProxyConfigProvider.cs`
- Implements `IProxyConfigProvider`
- Loads routes and clusters from database
- Provides configuration to YARP
#### 5. Tenant Routing Middleware
**File**: `src/YarpGateway/Middleware/TenantRoutingMiddleware.cs`
- Extracts tenant ID from JWT headers (`X-Tenant-Id`)
- Uses `RouteCache` to get route cluster
- Sets `context.Items["DynamicClusterId"]` for YARP
### Phase 3: Frontend Development
Created Vue3 admin pages:
1. **Dashboard.vue** - Statistics dashboard
2. **TenantList.vue** - Tenant management
3. **TenantRoutes.vue** - Tenant-specific routes
4. **GlobalRoutes.vue** - Global routes management (NEW)
5. **ClusterInstances.vue** - Service instance management
### Phase 4: Bug Fixes
1. Fixed 4K screen width constraints (removed max-width, added 100% width/height)
2. Fixed route `/tenants` not displaying until page refresh
3. Fixed CORS configuration (allowed origins: localhost:5173, 127.0.0.1:5173)
4. Fixed multiple compilation errors in `DistributedWeightedRoundRobinPolicy.cs`:
- Missing using statements
- JsonSerializer ambiguity (used full namespace)
- HashCode.Combine signature errors
- Typo: `M.achineName``MachineName`
- Changed `await using var` to `using var`
### Phase 5: Database Configuration
Applied database migration:
```bash
dotnet ef database update
```
## Current Status
### ✅ Completed
1. Backend compiles successfully
2. Frontend runs on http://localhost:5173
3. Database migrations applied
4. Global routes management UI created
5. Redis distributed load balancing implemented
6. In-memory route caching implemented
### ⚠️ Known Issue: YARP Dynamic Routing
**Problem**:
- Accessing `/api/product/test` returns 404
- Logs show: "Request reached end of middleware pipeline"
- `DynamicProxyConfigProvider` is registered but not being used by YARP
**User's comment**:
> "你等下 下游的应用还没建立 肯定404吧"
> "你先把网关的ui更新一下功能呀。这个你先别急着测试吧"
**Conclusion**: The 404 is expected because downstream microservices don't exist yet. User wants to focus on UI updates and move to microservices analysis.
## Database Schema
### Tables
#### Tenants
```sql
Id: bigint (PK)
TenantCode: varchar(50) (unique)
TenantName: varchar(100)
Status: int (1=enabled, 0=disabled)
IsDeleted: boolean
CreatedTime, UpdatedTime, Version
```
#### TenantRoutes
```sql
Id: bigint (PK)
TenantCode: varchar(50) (empty string = global route)
ServiceName: varchar(100)
ClusterId: varchar(100)
PathPattern: varchar(200)
Priority: int (0=global, 10=tenant)
Status: int
IsGlobal: boolean (NEW)
IsDeleted: boolean
CreatedTime, UpdatedTime, Version
```
#### ServiceInstances
```sql
Id: bigint (PK)
ClusterId: varchar(100)
DestinationId: varchar(100)
Address: varchar(200)
Health: int (1=healthy)
Weight: int
Status: int
IsDeleted: boolean
CreatedTime, UpdatedTime, Version
```
## Configuration
### Backend (appsettings.json)
```json
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
},
"Redis": {
"ConnectionString": "192.168.100.10:6379",
"Database": 0,
"InstanceName": "YarpGateway"
},
"Cors": {
"AllowedOrigins": ["http://localhost:5173", "http://127.0.0.1:5173", "http://localhost:5174"],
"AllowAnyOrigin": false
},
"ReverseProxy": {
"Routes": {},
"Clusters": {}
}
}
```
### Ports
- Frontend: http://localhost:5173
- Backend: http://0.0.0.0:8080
## API Endpoints
### Tenant Management
- `GET /api/gateway/tenants` - List tenants
- `POST /api/gateway/tenants` - Create tenant
- `DELETE /api/gateway/tenants/{id}` - Delete tenant
### Tenant Routes
- `GET /api/gateway/tenants/{tenantCode}/routes` - List tenant routes
- `POST /api/gateway/tenants/{tenantCode}/routes` - Create tenant route
### Global Routes
- `GET /api/gateway/routes/global` - List global routes
- `POST /api/gateway/routes/global` - Create global route
- `DELETE /api/gateway/routes/{id}` - Delete route
### Cluster Instances
- `GET /api/gateway/clusters/{clusterId}/instances` - List instances
- `POST /api/gateway/clusters/{clusterId}/instances` - Add instance
- `DELETE /api/gateway/instances/{id}` - Delete instance
### Configuration
- `POST /api/gateway/reload` - Reload configuration
## File Structure
```
/Users/movingsam/Fengling.Refactory.Buiding/src/
├── YarpGateway/
│ ├── Config/
│ │ ├── DatabaseRouteConfigProvider.cs
│ │ ├── DatabaseClusterConfigProvider.cs
│ │ ├── JwtConfig.cs
│ │ └── RedisConfig.cs
│ ├── Controllers/
│ │ └── GatewayConfigController.cs
│ ├── Data/
│ │ ├── GatewayDbContext.cs
│ │ └── GatewayDbContextFactory.cs
│ ├── DynamicProxy/
│ │ └── DynamicProxyConfigProvider.cs
│ ├── LoadBalancing/
│ │ └── DistributedWeightedRoundRobinPolicy.cs
│ ├── Middleware/
│ │ ├── JwtTransformMiddleware.cs
│ │ └── TenantRoutingMiddleware.cs
│ ├── Models/
│ │ ├── GwTenant.cs
│ │ ├── GwTenantRoute.cs
│ │ └── GwServiceInstance.cs
│ ├── Services/
│ │ ├── RouteCache.cs
│ │ └── RedisConnectionManager.cs
│ ├── Migrations/
│ │ ├── 20260201120312_InitialCreate.cs
│ │ └── 20260201133826_AddIsGlobalToTenantRoute.cs
│ ├── sql/
│ │ └── init.sql
│ ├── Program.cs
│ └── appsettings.json
└── YarpGateway.Admin/
├── src/
│ ├── api/
│ │ └── index.ts
│ ├── components/
│ │ └── Layout.vue
│ ├── stores/
│ │ └── tenant.ts
│ ├── views/
│ │ ├── Dashboard.vue
│ │ ├── TenantList.vue
│ │ ├── TenantRoutes.vue
│ │ ├── GlobalRoutes.vue
│ │ └── ClusterInstances.vue
│ ├── router/
│ │ └── index.ts
│ └── main.ts
└── package.json
```
## What We're Doing Now
The user wants to stop working on the gateway routing issue and instead focus on:
1. Updating the gateway UI functionality (COMPLETED - GlobalRoutes.vue added)
2. Analyzing the old backend for microservices split
User's exact words:
> "你先把网关的ui更新一下功能呀。这个你先别急着测试吧"
> "我准备开始分析业务的微服务拆分了"
## Next Steps (For New Conversation)
### Immediate Priority: Microservices Analysis
**Task**: Analyze the old backend (`/Users/movingsam/Fengling.Refactory/Fengling.Backend.Web/`) to determine how to split it into microservices
**Known Old Backend Structure**:
```
/Users/movingsam/Fengling.Refactory/Fengling.Backend.Web/src/src/
├── account/ # Account module
├── activityplan/ # Activity planning
├── basis/ # Basic configuration
├── channel/ # Channel management
├── company/ # Company management
├── coupon/ # Coupon management
├── fieldConfig/ # Field configuration
├── flow/ # Workflow
├── gift/ # Gift management
├── integralConfig/ # Points configuration
├── member/ # Member management
├── promoter/ # Promoter management
├── qipei/ # Service matching
├── reports/ # Reports
├── riskManage/ # Risk management
└── [many more modules...]
```
### Analysis Goals
1. Identify business domain boundaries
2. Determine which modules should become independent microservices
3. Design inter-service communication patterns
4. Plan database splitting strategy
5. Consider shared services (auth, configuration, etc.)
### Deferred Tasks (Lower Priority)
1. Fix YARP `DynamicProxyConfigProvider` to properly integrate with YARP
2. Test dynamic routing with actual downstream services
3. Complete deployment architecture (Docker, Kubernetes)
## Key Technical Decisions
### 1. Why Global Routes + Tenant-Specific Routes?
**Reason**: 99% of tenants share the same services, only 1% need dedicated instances
**Benefit**: Drastically reduces configuration complexity
### 2. Why YARP?
- Microsoft official support
- High performance (based on Kestrel)
- Extensible (custom load balancing policies)
- Dynamic configuration support
### 3. Why Redis?
- Distributed locks for multi-instance scenarios
- Persistent load balancing state
- High performance (millisecond response)
## Important Notes for Continuation
1. **Database Access**: PostgreSQL at 192.168.100.10:5432, Database: fengling_gateway
2. **Redis Access**: 192.168.100.10:6379
3. **Project Location**: `/Users/movingsam/Fengling.Refactory.Buiding/`
4. **User Preference**: Manually handles database migrations (user applies SQL manually)
5. **Old Gateway**: User confirmed the old gateway at `/Users/movingsam/Fengling.Refactory/Yarp.Gateway/` is no longer relevant for reference
---

370
README.md
View File

@ -1,370 +0,0 @@
# YARP Gateway - 租户路由网关
基于YARP的租户感知API网关支持JWT解析、动态租户路由、加权负载均衡。
## 功能特性
- ✅ JWT解析与Header传递
- ✅ 基于租户的动态路由
- ✅ 加权轮询负载均衡
- ✅ PostgreSQL配置持久化
- ✅ RESTful API管理接口
- ✅ Docker Compose部署
- ✅ Serilog结构化日志
- ✅ Prometheus指标导出
## 架构设计
```
客户端请求 → JWT解析中间件 → 租户路由中间件 → YARP网关 → 后端微服务
```
### 路由流程
1. 客户端携带JWT访问 `/api/product/list`
2. JWT解析中间件提取租户ID`customerA`
3. 添加Header: `X-Tenant-Id: customerA`
4. 租户路由中间件根据路径提取服务名(`product`
5. 动态构造Cluster ID: `customerA-product`
6. YARP将请求转发到该Cluster下的服务实例
7. 加权轮询策略选择实例
## 快速开始
### 前置要求
- .NET 10 SDK
- PostgreSQL 16+
- Docker & Docker Compose可选
### 本地开发
1. **创建数据库**
```bash
psql -h 192.168.100.10 -U postgres -c "CREATE DATABASE fengling_gateway;"
```
2. **执行迁移**
```bash
psql -h 192.168.100.10 -U postgres -d fengling_gateway -f sql/init.sql
```
3. **运行网关**
```bash
cd src/YarpGateway
dotnet run
```
4. **测试请求**
```bash
# 生成测试JWT需要配置Auth服务
# 然后测试请求
curl -H "Authorization: Bearer <JWT_TOKEN>" \
http://localhost:8080/api/product/list
```
### Docker部署
1. **启动所有服务**
```bash
cd docker
docker-compose up -d
```
2. **查看日志**
```bash
docker-compose logs -f gateway
```
3. **停止服务**
```bash
docker-compose down
```
## API接口
### 租户管理
**获取所有租户**
```http
GET /api/gateway/tenants
```
**创建租户**
```http
POST /api/gateway/tenants
Content-Type: application/json
{
"tenantCode": "customerC",
"tenantName": "客户C"
}
```
**删除租户**
```http
DELETE /api/gateway/tenants/{id}
```
### 路由管理
**获取租户路由**
```http
GET /api/gateway/tenants/{tenantCode}/routes
```
**创建路由**
```http
POST /api/gateway/tenants/{tenantCode}/routes
Content-Type: application/json
{
"serviceName": "product",
"pathPattern": "/api/product/{**catch-all}"
}
```
**删除路由**
```http
DELETE /api/gateway/routes/{id}
```
### 服务实例管理
**获取实例列表**
```http
GET /api/gateway/clusters/{clusterId}/instances
```
**添加实例**
```http
POST /api/gateway/clusters/{clusterId}/instances
Content-Type: application/json
{
"destinationId": "product-3",
"address": "http://customerA-product-3:8001",
"weight": 2
}
```
**删除实例**
```http
DELETE /api/gateway/instances/{id}
```
### 配置热更新
```http
POST /api/gateway/reload
```
## JWT格式要求
### 必需Claims
```json
{
"tenant": "customerA",
"sub": "123456",
"unique_name": "张三",
"role": ["admin", "user"]
}
```
### Header转换
JWT解析后以下Header会自动添加到请求中
- `X-Tenant-Id`: 租户ID
- `X-User-Id`: 用户ID
- `X-User-Name`: 用户名
- `X-Roles`: 角色列表(逗号分隔)
## 负载均衡策略
### 加权轮询 (WeightedRoundRobin)
权重高的实例获得更多流量分配。
**配置权重**
```bash
POST /api/gateway/clusters/customerA-product/instances
{
"destinationId": "product-1",
"address": "http://customerA-product-1:8001",
"weight": 10 # 权重10
}
```
**默认权重**: 1
## 数据库表结构
### gw_tenant
租户基础信息表
| 字段 | 类型 | 说明 |
|------|------|------|
| Id | BIGINT | 主键 |
| TenantCode | VARCHAR(50) | 租户编码(唯一) |
| TenantName | VARCHAR(100) | 租户名称 |
| Status | INTEGER | 状态1=启用 0=禁用 |
### gw_tenant_route
租户服务路由配置表
| 字段 | 类型 | 说明 |
|------|------|------|
| Id | BIGINT | 主键 |
| TenantCode | VARCHAR(50) | 租户编码 |
| ServiceName | VARCHAR(100) | 服务名称 |
| ClusterId | VARCHAR(100) | YARP Cluster ID |
| PathPattern | VARCHAR(200) | 路径匹配模式 |
### gw_service_instance
服务实例配置表
| 字段 | 类型 | 说明 |
|------|------|------|
| Id | BIGINT | 主键 |
| ClusterId | VARCHAR(100) | Cluster ID |
| DestinationId | VARCHAR(100) | Destination ID |
| Address | VARCHAR(200) | 服务地址 |
| Weight | INTEGER | 权重 |
| Health | INTEGER | 健康状态1=健康 0=不健康 |
## 监控和日志
### 日志位置
- **控制台**: 实时输出
- **文件**: `logs/gateway-{Date}.log`
### Prometheus指标
默认导出到 `/metrics` 端点(需添加 Prometheus 包)
**可用指标**:
- `gateway_requests_total`: 请求总数(按租户、服务、状态码分组)
- `gateway_request_duration_seconds`: 请求耗时(按租户、服务分组)
## 配置说明
### appsettings.json
```json
{
"ConnectionStrings": {
"DefaultConnection": "Host=postgres;Port=5432;Database=fengling_gateway;..."
},
"Jwt": {
"Authority": "https://your-auth-server.com",
"Audience": "fengling-gateway"
},
"ReverseProxy": {
"Routes": { "catch-all-route": { ... } },
"Clusters": { "dynamic-cluster": { ... } }
},
"Serilog": { ... }
}
```
### 环境变量
- `ConnectionStrings__DefaultConnection`: 数据库连接字符串
- `Jwt__Authority`: JWT认证服务器地址
- `Jwt__Audience`: JWT受众
## 性能调优
### 连接池配置
```csharp
services.AddDbContext<GatewayDbContext>(options =>
options.UseNpgsql(connectionString, o =>
{
o.CommandTimeout(30);
o.MaxBatchSize(100);
}));
```
### 日志级别优化
生产环境建议:
```json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning",
"Yarp.ReverseProxy": "Information"
}
}
}
```
## 故障排查
### 常见问题
**1. 路由404**
- 检查 `gw_tenant_route` 表是否有对应路由配置
- 检查JWT中的tenant claim是否正确
**2. 数据库连接失败**
- 验证连接字符串是否正确
- 检查PostgreSQL是否启动
- 检查防火墙设置
**3. 负载均衡不均**
- 检查实例权重配置
- 检查实例健康状态
## 项目结构
```
src/YarpGateway/
├── Config/ # 配置提供者
│ ├── JwtConfig.cs
│ ├── DatabaseRouteConfigProvider.cs
│ └── DatabaseClusterConfigProvider.cs
├── Controllers/ # API控制器
│ └── GatewayConfigController.cs
├── Data/ # 数据库上下文
│ └── GatewayDbContext.cs
├── LoadBalancing/ # 负载均衡策略
│ └── WeightedRoundRobinPolicy.cs
├── Middleware/ # 中间件
│ ├── JwtTransformMiddleware.cs
│ └── TenantRoutingMiddleware.cs
├── Metrics/ # 监控指标
│ └── GatewayMetrics.cs
├── Models/ # 数据模型
│ ├── GwTenant.cs
│ ├── GwTenantRoute.cs
│ └── GwServiceInstance.cs
├── appsettings.json
├── Dockerfile
└── Program.cs
```
## 开发计划
- [ ] 添加Prometheus指标导出
- [ ] 实现Vue3管理界面
- [ ] 添加限流策略
- [ ] 添加熔断机制
- [ ] 实现配置中心集成
- [ ] 添加服务发现集成
## 许可证
MIT License

View File

@ -1,20 +0,0 @@
version: '3.8'
services:
gateway:
build: ./src/YarpGateway
container_name: fengling-gateway
ports:
- "8080:8080"
environment:
- ConnectionStrings__DefaultConnection=Host=192.168.100.10;Port=5432;Database=fengling_gateway;Username=postgres;Password=postgres
- Jwt__Authority=https://your-auth-server.com
volumes:
- ./logs:/app/logs
networks:
- fengling-network
restart: unless-stopped
networks:
fengling-network:
driver: bridge

File diff suppressed because it is too large Load Diff

View File

@ -1,216 +0,0 @@
# Fengling Microservices Architecture Design
**Date**: 2025-02-01
**Status**: Approved
## Overview
风灵(Fengling)系统从单体架构重构为微服务架构,采用核心业务微服务拆分策略,每服务独立数据库,通过RabbitMQ异步通信。
## Core Business Services
### 1. 会员服务
- **Database**: `fengling_member`
- **Entities**: Member, MemberLevel, MemberTag, MemberGroup
- **Responsibilities**: 会员信息管理、会员等级、会员分组、标签管理
### 2. 推广员服务
- **Database**: `fengling_promoter`
- **Entities**: Promoter, PromotersActivity, PromoterStore
- **Responsibilities**: 推广员管理、推广员活动、推广员积分
### 3. 营销活动服务
- **Database**: `fengling_activity`
- **Entities**: Activity, ActivityAward, ActivitySign, AfeActivity
- **Responsibilities**: 活动创建、活动报名、签到、抽奖、礼品发放
### 4. 优惠券服务
- **Database**: `fengling_coupon`
- **Entities**: Coupon, CouponExpense, CouponTask
- **Responsibilities**: 优惠券发放、核销、任务配置
### 5. 礼品服务
- **Database**: `fengling_gift`
- **Entities**: Gift, GiftCategory, GiftExpense
- **Responsibilities**: 礼品管理、礼品发放记录
### 6. 订单服务
- **Database**: `fengling_order`
- **Entities**: Order, VirtualOrder, StoreshopOrders
- **Responsibilities**: 订单创建、订单状态管理
### 7. 渠道服务
- **Database**: `fengling_channel`
- **Entities**: Channel, ChannelQrCode, ChannelApply, ChannelTag
- **Responsibilities**: 渠道管理、二维码生成、渠道申请审核
### 8. 门店服务
- **Database**: `fengling_store`
- **Entities**: Store, StoreCategory, StoreLevel, StoreApply
- **Responsibilities**: 门店管理、门店等级、门店申请审核
### 9. 账户服务
- **Database**: `fengling_account`
- **Entities**: Account, CustomerBalance, WalletBalance
- **Responsibilities**: 资金账户、积分账户、钱包余额管理
### 10. 积分服务
- **Database**: `fengling_points`
- **Entities**: Points, IntegralRule, IntegralDetail, PointClearConfig
- **Responsibilities**: 积分规则配置、积分发放/扣减、积分明细
## Infrastructure Services
### 1. 认证授权服务
- **Technology**: OpenIddict (开源免费)
- **Responsibilities**:
- 用户认证(JWT Token签发)
- OAuth2/OIDC标准支持
- 多租户认证(TenantId嵌入Token)
- 权限验证
- **Gateway Integration**: 网关验证Token并传递TenantId到下游服务
### 2. 配置管理
- **Approach**: K8s ConfigMap + appsettings环境变量
- **Shared Library**: `Fengling.Configuration`
- 统一配置读取
- 环境变量覆盖支持
- 无需额外部署
- **Benefits**: 简单可靠,零额外组件
### 3. 日志服务
- **Technology**: Serilog + 云厂商日志服务
- **Collection**:
- 应用输出JSON到stdout
- 云厂商Agent抓取日志
- **Format**: JSON结构化(TraceId, SpanId, TenantId)
### 4. 链路追踪
- **Technology**: OpenTelemetry + Jaeger
- **Scope**: HTTP/RabbitMQ/DB/Redis
- **Retention**: 30天
### 5. 消息队列
- **Technology**: RabbitMQ (集群部署)
- **Exchanges**:
- `activity.exchange`: 营销活动相关消息
- `order.exchange`: 订单相关消息
- `member.exchange`: 会员相关消息
- `points.exchange`: 积分相关消息
- **Persistence**: 开启持久化
- **Dead Letter Queue**: 每个队列配置DLQ
## Communication Pattern
**All services use RabbitMQ for asynchronous communication**
### Message Flows
1. **Order Created**:
- Order Service → `order.exchange` → Points Service (add points)
- Order Service → `order.exchange` → Coupon Service (consume coupon)
2. **Activity Signed**:
- Activity Service → `activity.exchange` → Points Service (add sign points)
- Activity Service → `activity.exchange` → Gift Service (issue gift)
3. **Member Registered**:
- Member Service → `member.exchange` → Channel Service (bind channel)
- Member Service → `member.exchange` → Points Service (init account)
## Database Strategy
**Each microservice has its own PostgreSQL database**
- Naming convention: `fengling_<service_name>`
- No cross-service joins allowed
- Data consistency via eventual consistency (message queue)
- Tenant isolation via `TenantId` column in all tables
## Security
1. **Authentication**: JWT Token via OpenIddict
2. **Authorization**: Role-based access control (RBAC)
3. **Tenant Isolation**: TenantId in JWT + TenantId column in all tables
4. **API Security**: Gateway validates all incoming requests
## Deployment
- **Infrastructure**: Kubernetes
- **Gateway**: YARP Gateway (already implemented)
- **Load Balancing**: Kubernetes Service + Ingress
- **Configuration**: K8s ConfigMap
- **Logging**: Cloud provider log aggregation
- **Monitoring**: Prometheus + Grafana (optional)
## Implementation Priority
### Phase 1: Infrastructure (Current)
1. ✅ YARP Gateway
2. 🔄 Authentication Service (in progress)
3. RabbitMQ Setup
4. OpenTelemetry + Jaeger Setup
### Phase 2: Core Services
5. Member Service
6. Promoter Service
7. Activity Service
8. Order Service
### Phase 3: Supporting Services
9. Coupon Service
10. Gift Service
11. Channel Service
12. Store Service
13. Account Service
14. Points Service
## Technology Stack
- **.NET Version**: .NET 9.0
- **Language**: C# 13
- **Database**: PostgreSQL
- **ORM**: Entity Framework Core 9.0
- **Cache**: Redis (StackExchange.Redis)
- **Message Queue**: RabbitMQ (MassTransit)
- **Authentication**: OpenIddict
- **Logging**: Serilog
- **Tracing**: OpenTelemetry
- **API Gateway**: YARP
- **Container**: Docker
- **Orchestration**: Kubernetes
## Reference Architecture
```
[Client App]
|
v
[YARP Gateway]
|
|---[Tenant Routing]--->
|
[Authentication Service] (OpenIddict)
|
v
[Service Mesh (RabbitMQ)]
|
+---[Member Service]--->[fengling_member DB]
+---[Promoter Service]-->[fengling_promoter DB]
+---[Activity Service]-->[fengling_activity DB]
+---[Order Service]----->[fengling_order DB]
+---[Coupon Service]--->[fengling_coupon DB]
+---[Gift Service]----->[fengling_gift DB]
+---[Channel Service]-->[fengling_channel DB]
+---[Store Service]---->[fengling_store DB]
+---[Account Service]-->[fengling_account DB]
+---[Points Service]--->[fengling_points DB]
```
## Migration Strategy
1. **Phase 1**: Extract shared libraries (Configuration, Logging, Tracing)
2. **Phase 2**: Implement Authentication Service
3. **Phase 3**: Extract services one by one (least dependent first)
4. **Phase 4**: Migrate data from monolithic database
5. **Phase 5**: Update Gateway routing to new services
6. **Phase 6**: Decommission old monolithic application

View File

@ -1,203 +0,0 @@
# Fengling.Console 运管中心规划
## 项目概述
**项目名称**: Fengling.Console
**中文名称**: 风铃运管中心
**定位**: 统一运维管理平台前端Vue项目
## 架构设计
### 前端(新建)
- **项目**: `src/Fengling.Console.Web/` (Vue 3 + Vite)
- **功能**:
- 网关路由管理
- 租户管理
- OAuth Client管理
- 用户管理
- 集群实例管理
### 后端API已有
1. **Fengling.AuthService** - 认证服务
- OAuth Client管理 API
- 用户管理 API
- Token 端点
2. **YarpGateway** - 网关服务
- 租户管理 API
- 路由管理 API
- 集群实例管理 API
## 功能模块
### 模块1: 认证登录
- OAuth2 授权码流登录
- Token刷新机制
- 登出功能
- Client: fengling-console
### 模块2: 网关管理
- 租户列表
- 租户路由配置
- 集群实例管理
- 全局路由配置
- 负载均衡策略
### 模块3: OAuth Client管理
- Client ID/Secret管理
- 重定向URI配置
- 授权类型配置
- Scope配置
- Client状态管理
### 模块4: 用户管理
- 用户列表
- 用户角色分配
- 租户分配
- 用户状态管理
## 认证集成
### Fengling.Console作为OAuth Client
已在AuthService中预注册
```yaml
ClientId: "fengling-console"
ClientSecret: "console-secret-change-in-production"
RedirectUris: ["http://console.fengling.local/auth/callback"]
PostLogoutRedirectUris: ["http://console.fengling.local/"]
Scopes: ["api", "offline_access"]
GrantTypes: ["authorization_code", "refresh_token"]
```
## 目录结构
```
src/
├── Fengling.AuthService/ # 认证服务(已完成)
│ ├── Controllers/
│ │ ├── OAuthClientsController.cs # OAuth Client管理API
│ │ └── AuthController.cs # 认证API
│ ├── Models/
│ │ ├── OAuthApplication.cs
│ │ ├── ApplicationUser.cs
│ │ └── ApplicationRole.cs
│ └── Data/
│ ├── SeedData.cs # 预注册fengling-console
│ └── ApplicationDbContext.cs
├── YarpGateway/ # 网关服务(已有)
│ ├── Controllers/
│ │ ├── GatewayConfigController.cs
│ │ ├── TenantController.cs
│ │ └── ClusterInstanceController.cs
│ ├── Models/
│ │ ├── GwTenant.cs
│ │ ├── GwTenantRoute.cs
│ │ └── GwServiceInstance.cs
│ └── Data/
│ └── GatewayDbContext.cs
└── Fengling.Console.Web/ # 运管中心前端(新建)
├── src/
│ ├── views/
│ │ ├── Auth/ # 认证相关
│ │ │ ├── Login.vue
│ │ │ └── Callback.vue
│ │ ├── Gateway/ # 网关管理
│ │ │ ├── TenantList.vue
│ │ │ ├── TenantRoutes.vue
│ │ │ ├── ClusterInstances.vue
│ │ │ └── GlobalRoutes.vue
│ │ ├── OAuth/ # OAuth管理
│ │ │ └── ClientList.vue
│ │ └── Users/ # 用户管理
│ │ └── UserList.vue
│ ├── components/
│ ├── api/
│ │ ├── auth.ts # AuthService API
│ │ ├── gateway.ts # Gateway API
│ │ └── oauth.ts # OAuth API
│ ├── stores/
│ │ ├── auth.ts
│ │ └── user.ts
│ └── router/
│ └── index.ts
├── package.json
└── vite.config.ts
```
## API调用关系
```
Fengling.Console.Web (前端)
├── OAuth2登录 → Fengling.AuthService (/connect/authorize)
├── OAuth Client管理 → Fengling.AuthService (/api/oauthclients)
├── 用户管理 → Fengling.AuthService (/api/users)
├── 租户管理 → YarpGateway (/api/tenants)
├── 路由管理 → YarpGateway (/api/routes)
└── 集群管理 → YarpGateway (/api/instances)
```
## 技术栈
### 前端 (Fengling.Console.Web)
- Vue 3 + TypeScript
- Vite
- Element Plus (UI组件库)
- Pinia (状态管理)
- Vue Router
- Axios (HTTP客户端)
### 后端API
- Fengling.AuthService: .NET 10.0, OpenIddict 7.2.0
- YarpGateway: .NET 10.0, YARP 2.2.0
## 实施计划
### Phase 1: 认证服务扩展(已完成)
- [x] Task 1-9: 完成AuthService基础功能
- [x] Task 10: 添加OAuth Client模型和管理API
- [x] Task 11: 预注册Fengling.Console作为Client
### Phase 2: 运管中心前端(待开始)
- [ ] Task 12: 创建Fengling.Console.Web Vue项目
- [ ] Task 13: 配置项目结构和依赖
- [ ] Task 14: 实现OAuth2登录流程
- [ ] Task 15: 实现网关管理界面迁移YarpGateway.Admin
- [ ] Task 16: 实现OAuth Client管理界面
- [ ] Task 17: 实现用户管理界面
- [ ] Task 18: 配置API封装调用AuthService和YarpGateway
- [ ] Task 19: 添加Dockerfile和部署配置
## 依赖关系
```
Fengling.Console.Web (Vue前端)
↓ (OAuth2)
Fengling.AuthService (认证API)
↓ (JWT Token验证)
YarpGateway (网关API)
```
## 环境配置
### 开发环境
```env
VITE_AUTH_SERVICE_URL=http://auth.fengling.local:5000
VITE_GATEWAY_SERVICE_URL=http://gateway.fengling.local:5000
VITE_CLIENT_ID=fengling-console
VITE_REDIRECT_URI=http://console.fengling.local:5173/auth/callback
```
### 生产环境
```env
VITE_AUTH_SERVICE_URL=https://auth.fengling.local
VITE_GATEWAY_SERVICE_URL=https://gateway.fengling.local
```
## 注意事项
1. **无后端项目**: Fengling.Console是纯前端项目
2. **直接调用现有API**: 复用AuthService和YarpGateway的API
3. **OAuth2认证**: 使用授权码流登录
4. **Token管理**: 自动刷新access token
5. **跨域处理**: 配置CORS或使用同域代理

View File

@ -1,100 +0,0 @@
# Task 1: Create Project Structure
## Task Description
**Files:**
- Create: `src/Fengling.AuthService/Fengling.AuthService.csproj`
- Create: `src/Fengling.AuthService/Program.cs`
- Create: `src/Fengling.AuthService/appsettings.json`
## Implementation Steps
### Step 1: Create project file
Run:
```bash
cd /Users/movingsam/Fengling.Refactory.Buiding/src
dotnet new webapi -n Fengling.AuthService -o Fengling.AuthService
```
### Step 2: Update project file with dependencies
Edit: `src/Fengling.AuthService/Fengling.AuthService.csproj`
```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenIddict.AspNetCore" Version="7.2.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="7.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="OpenTelemetry" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.0" />
</ItemGroup>
</Project>
```
### Step 3: Create appsettings.json
Create: `src/Fengling.AuthService/appsettings.json`
```json
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542"
},
"OpenIddict": {
"Issuer": "https://auth.fengling.local",
"Audience": "fengling-api"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
```
### Step 4: Commit
```bash
git add src/Fengling.AuthService/
git commit -m "feat(auth): create authentication service project structure"
```
## Context
This is the first task of implementing the Fengling Authentication Service. We're building a standalone authentication service using OpenIddict for JWT token issuance and multi-tenant support. This task establishes the basic ASP.NET Core Web API project structure with all necessary dependencies including OpenIddict, EF Core, PostgreSQL, Serilog, and OpenTelemetry.
**Architecture**: ASP.NET Core Web API with OpenIddict for OAuth2/OIDC, PostgreSQL for user data.
**Tech Stack**: .NET 10.0, OpenIddict 7.2.0, EF Core 10.0.2, PostgreSQL, Serilog 9.0.0, OpenTelemetry 1.11.0.
**Working Directory**: `/Users/movingsam/Fengling.Refactory.Buiding/src`
## Verification
- [ ] Project file created successfully
- [ ] Target framework set to net10.0
- [ ] All NuGet packages added (latest versions)
- [ ] appsettings.json configured with connection string and OpenIddict settings
- [ ] Project builds successfully
- [ ] Committed to git
## Notes
- OpenIddict packages updated to 7.2.0 (latest)
- All dependencies updated to latest versions
- Used .NET 10.0 as target framework

View File

@ -1,128 +0,0 @@
# Task 2: Create Database Models
## Task Description
**Files:**
- Create: `src/Fengling.AuthService/Data/ApplicationDbContext.cs`
- Create: `src/Fengling.AuthService/Models/ApplicationUser.cs`
- Create: `src/Fengling.AuthService/Models/ApplicationRole.cs`
- Create: `src/Fengling.AuthService/Data/Migrations/20250201_InitialCreate.cs`
## Implementation Steps
### Step 1: Create ApplicationUser model
Create: `src/Fengling.AuthService/Models/ApplicationUser.cs`
```csharp
using Microsoft.AspNetCore.Identity;
namespace Fengling.AuthService.Models;
public class ApplicationUser : IdentityUser<long>
{
public string? RealName { get; set; }
public string? Phone { get; set; }
public long TenantId { get; set; }
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedTime { get; set; }
public bool IsDeleted { get; set; }
}
```
### Step 2: Create ApplicationRole model
Create: `src/Fengling.AuthService/Models/ApplicationRole.cs`
```csharp
using Microsoft.AspNetCore.Identity;
namespace Fengling.AuthService.Models;
public class ApplicationRole : IdentityRole<long>
{
public string? Description { get; set; }
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
}
```
### Step 3: Create ApplicationDbContext
Create: `src/Fengling.AuthService/Data/ApplicationDbContext.cs`
```csharp
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Data;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, long>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<ApplicationUser>(entity =>
{
entity.Property(e => e.RealName).HasMaxLength(100);
entity.Property(e => e.Phone).HasMaxLength(20);
entity.HasIndex(e => e.TenantId);
entity.HasIndex(e => e.Phone).IsUnique();
});
builder.Entity<ApplicationRole>(entity =>
{
entity.Property(e => e.Description).HasMaxLength(200);
});
}
}
```
### Step 4: Add migration
Run:
```bash
cd /Users/movingsam/Fengling.Refactory.Buiding/src/Fengling.AuthService
dotnet ef migrations add InitialCreate -o Data/Migrations
```
### Step 5: Update database
Run:
```bash
dotnet ef database update
```
### Step 6: Commit
```bash
git add src/Fengling.AuthService/Models/ src/Fengling.AuthService/Data/
git commit -m "feat(auth): add user and role models with EF Core migrations"
```
## Context
This task creates the database models for users and roles with multi-tenant support. We're using ASP.NET Core Identity with long primary keys (IdentityUser<long>) and adding custom properties like RealName, Phone, and TenantId for multi-tenant isolation.
**Tech Stack**: EF Core 9.0, PostgreSQL, ASP.NET Core Identity
## Verification
- [ ] ApplicationUser model created with long key type and custom properties
- [ ] ApplicationRole model created with description property
- [ ] ApplicationDbContext configured with proper entity configurations
- [ ] EF Core migration generated successfully
- [ ] Database updated with schema
- [ ] Committed to git
## Notes
- Using long (Int64) as key type for better scalability
- TenantId added to ApplicationUser for multi-tenant support
- Phone number has unique index constraint

View File

@ -1,202 +0,0 @@
# Task 3: Configure OpenIddict
## Task Description
**Files:**
- Create: `src/Fengling.AuthService/Configuration/OpenIddictSetup.cs`
- Modify: `src/Fengling.AuthService/Program.cs`
## Implementation Steps
### Step 1: Create OpenIddict configuration
Create: `src/Fengling.AuthService/Configuration/OpenIddictSetup.cs`
```csharp
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Validation.AspNetCore;
namespace Fengling.AuthService.Configuration;
public static class OpenIddictSetup
{
public static IServiceCollection AddOpenIddictConfiguration(this IServiceCollection services, IConfiguration configuration)
{
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<Data.ApplicationDbContext>();
})
.AddServer(options =>
{
options.SetIssuer(configuration["OpenIddict:Issuer"] ?? "https://auth.fengling.local");
options.AddSigningKey(new SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes("fengling-super-secret-key-for-dev-only-change-in-prod-please!!!")));
options.AllowAuthorizationCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.RequireProofKeyForCodeExchange();
options.RegisterScopes("api", "offline_access");
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassThrough()
.EnableTokenEndpointPassThrough()
.EnableLogoutEndpointPassThrough();
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
return services;
}
}
```
### Step 2: Update Program.cs with OpenIddict and EF Core
Edit: `src/Fengling.AuthService/Program.cs`
```csharp
using Fengling.AuthService.Configuration;
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Serilog
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
builder.Host.UseSerilog();
// Database
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Identity
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// OpenIddict
builder.Services.AddOpenIddictConfiguration(builder.Configuration);
// OpenTelemetry
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService("Fengling.AuthService"))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource("OpenIddict.Server.AspNetCore")
.AddOtlpExporter();
// Controllers
builder.Services.AddControllers();
// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Fengling Auth Service",
Version = "v1",
Description = "Authentication and authorization service using OpenIddict"
});
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Password = new OpenApiOAuthFlow
{
TokenUrl = "/connect/token"
}
}
});
});
var app = builder.Build();
// Configure pipeline
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Fengling Auth Service v1");
options.OAuthClientId("swagger-ui");
options.OAuthUsePkce();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
```
### Step 3: Run to verify startup
Run:
```bash
dotnet run
```
Expected: Service starts without errors, Swagger UI available at http://localhost:5000/swagger
### Step 4: Commit
```bash
git add src/Fengling.AuthService/Configuration/ src/Fengling.AuthService/Program.cs
git commit -m "feat(auth): configure OpenIddict with JWT and OAuth2 support"
```
## Context
This task configures OpenIddict for OAuth2/OIDC authentication, including:
- Server configuration with token endpoints
- Password flow for user authentication
- Development certificates for signing
- Integration with ASP.NET Core Identity
- Swagger UI with OAuth2 support
**Tech Stack**: OpenIddict 7.2.0, ASP.NET Core Identity, Swagger
## Verification
- [ ] OpenIddictSetup class created
- [ ] Program.cs updated with OpenIddict configuration
- [ ] Service starts without errors
- [ ] Swagger UI accessible
- [ ] Committed to git
## Notes
- Using symmetric key for token signing (dev only)
- Password flow enabled for direct authentication
- Development certificates used (replace in production)

View File

@ -1,171 +0,0 @@
# Task 4: Create Auth Controller
## Task Description
**Files:**
- Create: `src/Fengling.AuthService/Controllers/AuthController.cs`
- Create: `src/Fengling.AuthService/DTOs/LoginRequest.cs`
- Create: `src/Fengling.AuthService/DTOs/LoginResponse.cs`
- Create: `src/Fengling.AuthService/DTOs/TokenResponse.cs`
## Implementation Steps
### Step 1: Create DTOs
Create: `src/Fengling.AuthService/DTOs/LoginRequest.cs`
```csharp
namespace Fengling.AuthService.DTOs;
public class LoginRequest
{
public string UserName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public long TenantId { get; set; }
}
```
Create: `src/Fengling.AuthService/DTOs/LoginResponse.cs`
```csharp
namespace Fengling.AuthService.DTOs;
public class LoginResponse
{
public string AccessToken { get; set; } = string.Empty;
public string RefreshToken { get; set; } = string.Empty;
public int ExpiresIn { get; set; }
public string TokenType { get; set; } = "Bearer";
}
```
### Step 2: Create AuthController
Create: `src/Fengling.AuthService/Controllers/AuthController.cs`
```csharp
using Fengling.AuthService.DTOs;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly ILogger<AuthController> _logger;
public AuthController(
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager,
ILogger<AuthController> logger)
{
_signInManager = signInManager;
_userManager = userManager;
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_logger = logger;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await _userManager.FindByNameAsync(request.UserName);
if (user == null || user.IsDeleted)
{
return Unauthorized(new { error = "用户不存在" });
}
if (user.TenantId != request.TenantId)
{
return Unauthorized(new { error = "租户不匹配" });
}
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (!result.Succeeded)
{
return Unauthorized(new { error = "用户名或密码错误" });
}
var token = await GenerateTokenAsync(user);
return Ok(token);
}
private async Task<LoginResponse> GenerateTokenAsync(ApplicationUser user)
{
var claims = new List<System.Security.Claims.Claim>
{
new(Claims.Subject, user.Id.ToString()),
new(Claims.Name, user.UserName ?? string.Empty),
new(Claims.Email, user.Email ?? string.Empty),
new("tenant_id", user.TenantId.ToString())
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(Claims.Role, role));
}
var identity = new System.Security.Claims.ClaimsIdentity(claims, "Server");
var principal = new System.Security.Claims.ClaimsPrincipal(identity);
return new LoginResponse
{
AccessToken = "token-placeholder",
RefreshToken = "refresh-placeholder",
ExpiresIn = 3600,
TokenType = "Bearer"
};
}
}
```
### Step 3: Run to verify controller compilation
Run:
```bash
dotnet build
```
Expected: Build succeeds
### Step 4: Commit
```bash
git add src/Fengling.AuthService/Controllers/ src/Fengling.AuthService/DTOs/
git commit -m "feat(auth): add authentication controller with login endpoint"
```
## Context
This task creates an authentication controller with a login endpoint. The login endpoint validates user credentials, checks tenant isolation, and generates JWT tokens with embedded tenant_id claims for multi-tenant routing.
**Tech Stack**: ASP.NET Core Controllers, OpenIddict
## Verification
- [ ] LoginRequest DTO created
- [ ] LoginResponse DTO created
- [ ] AuthController created with login endpoint
- [ ] Tenant validation implemented
- [ ] Build succeeds
- [ ] Committed to git
## Notes
- Token generation is placeholder, will be replaced by OpenIddict in next task
- Tenant validation ensures multi-tenant isolation

View File

@ -1,186 +0,0 @@
# Task 5: Create OpenIddict Endpoints
## Task Description
**Files:**
- Create: `src/Fengling.AuthService/Controllers/AuthorizationController.cs`
## Implementation Steps
### Step 1: Create authorization endpoints
Create: `src/Fengling.AuthService/Controllers/AuthorizationController.cs`
```csharp
using Fengling.AuthService.Models;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Fengling.AuthService.Controllers;
public class AuthorizationController : Controller
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<AuthorizationController> _logger;
public AuthorizationController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
ILogger<AuthorizationController> logger)
{
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager;
_userManager = userManager;
_logger = logger;
}
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null || user.IsDeleted)
{
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户不存在"
}));
}
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (!result.Succeeded)
{
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户名或密码错误"
}));
}
var principal = await _signInManager.CreateUserPrincipalAsync(user);
var claims = new List<System.Security.Claims.Claim>
{
new(Claims.Subject, user.Id.ToString()),
new(Claims.Name, user.UserName ?? string.Empty),
new(Claims.Email, user.Email ?? string.Empty),
new("tenant_id", user.TenantId.ToString())
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(Claims.Role, role));
}
principal.SetScopes(request.GetScopes());
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.UnsupportedGrantType,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "不支持的授权类型"
}));
}
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
var result = await HttpContext.AuthenticateAsync();
if (result == null || !result.Succeeded)
{
return Challenge(
new AuthenticationProperties
{
RedirectUri = Request.Path + Request.QueryString
},
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
return Ok(new { message = "Authorization endpoint" });
}
[HttpPost("~/connect/logout")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}
```
### Step 2: Run to verify
Run:
```bash
dotnet run
```
Test with curl:
```bash
curl -X POST http://localhost:5000/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "username=admin" \
-d "password=Admin@123" \
-d "scope=api offline_access"
```
Expected: 400 error (user doesn't exist yet, but endpoint is working)
### Step 3: Commit
```bash
git add src/Fengling.AuthService/Controllers/AuthorizationController.cs
git commit -m "feat(auth): add OpenIddict authorization endpoints"
```
## Context
This task creates OpenIddict OAuth2/OIDC endpoints including token exchange, authorize, and logout. The token endpoint supports password flow for direct authentication.
**Tech Stack**: OpenIddict 7.2.0, ASP.NET Core
## Verification
- [ ] AuthorizationController created
- [ ] Token endpoint supports password flow
- [ ] Tenant ID added to token claims
- [ ] Build succeeds
- [ ] Committed to git
## Notes
- Token endpoint returns JWT with tenant_id claim
- Logout endpoint clears session

View File

@ -1,154 +0,0 @@
# Task 6: Create Seed Data
## Task Description
**Files:**
- Create: `src/Fengling.AuthService/Data/SeedData.cs`
- Modify: `src/Fengling.AuthService/Program.cs`
## Implementation Steps
### Step 1: Create seed data class
Create: `src/Fengling.AuthService/Data/SeedData.cs`
```csharp
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Data;
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider)
{
using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
context.Database.EnsureCreated();
var adminRole = await roleManager.FindByNameAsync("Admin");
if (adminRole == null)
{
adminRole = new ApplicationRole
{
Name = "Admin",
Description = "System administrator",
CreatedTime = DateTime.UtcNow
};
await roleManager.CreateAsync(adminRole);
}
var adminUser = await userManager.FindByNameAsync("admin");
if (adminUser == null)
{
adminUser = new ApplicationUser
{
UserName = "admin",
Email = "admin@fengling.local",
RealName = "系统管理员",
Phone = "13800138000",
TenantId = 1,
EmailConfirmed = true,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
};
var result = await userManager.CreateAsync(adminUser, "Admin@123");
if (result.Succeeded)
{
await userManager.AddToRoleAsync(adminUser, "Admin");
}
}
var testUser = await userManager.FindByNameAsync("testuser");
if (testUser == null)
{
testUser = new ApplicationUser
{
UserName = "testuser",
Email = "test@fengling.local",
RealName = "测试用户",
Phone = "13900139000",
TenantId = 1,
EmailConfirmed = true,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
};
var result = await userManager.CreateAsync(testUser, "Test@123");
if (result.Succeeded)
{
var userRole = new ApplicationRole
{
Name = "User",
Description = "普通用户",
CreatedTime = DateTime.UtcNow
};
await roleManager.CreateAsync(userRole);
await userManager.AddToRoleAsync(testUser, "User");
}
}
}
}
```
### Step 2: Update Program.cs to call seed data
Edit: `src/Fengling.AuthService/Program.cs` (add after `var app = builder.Build();`)
```csharp
using (var scope = app.Services.CreateScope())
{
await Data.SeedData.Initialize(scope.ServiceProvider);
}
```
### Step 3: Run to create seed data
Run:
```bash
dotnet run
```
Expected: Logs show admin user created successfully
### Step 4: Test login
Run:
```bash
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"userName":"admin","password":"Admin@123","tenantId":1}'
```
Expected: Returns token (placeholder for now)
### Step 5: Commit
```bash
git add src/Fengling.AuthService/Data/SeedData.cs src/Fengling.AuthService/Program.cs
git commit -m "feat(auth): add seed data for admin and test users"
```
## Context
This task creates initial seed data including admin and test users with default passwords. This allows immediate testing of the authentication service.
**Tech Stack**: ASP.NET Core Identity
## Verification
- [ ] SeedData class created
- [ ] Program.cs calls seed data on startup
- [ ] Admin user created: admin/Admin@123
- [ ] Test user created: testuser/Test@123
- [ ] Both users assigned to tenant 1
- [ ] Build succeeds
- [ ] Committed to git
## Notes
- Passwords should be changed in production
- All users assigned to tenant 1 for initial setup

View File

@ -1,77 +0,0 @@
# Task 7: Create Health Check Endpoint
## Task Description
**Files:**
- Modify: `src/Fengling.AuthService/Program.cs`
- Modify: `src/Fengling.AuthService/Fengling.AuthService.csproj`
## Implementation Steps
### Step 1: Add health check package
Edit: `src/Fengling.AuthService/Fengling.AuthService.csproj`
Add package reference:
```xml
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Npgsql.HealthChecks" Version="10.0.0" />
```
### Step 2: Add health check configuration
Edit: `src/Fengling.AuthService/Program.cs` (add after builder services)
```csharp
builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("DefaultConnection")!);
```
### Step 3: Add health check endpoint
Edit: `src/Fengling.AuthService/Program.cs` (before app.Run())
```csharp
app.MapHealthChecks("/health");
```
### Step 4: Test health check
Run:
```bash
dotnet run
```
Test:
```bash
curl http://localhost:5000/health
```
Expected: "Healthy"
### Step 5: Commit
```bash
git add src/Fengling.AuthService/Program.cs src/Fengling.AuthService/Fengling.AuthService.csproj
git commit -m "feat(auth): add health check endpoint"
```
## Context
This task adds a health check endpoint to monitor service and database connectivity. Health checks are essential for container orchestration and monitoring.
**Tech Stack**: ASP.NET Core Health Checks, PostgreSQL
## Verification
- [ ] Health check packages added
- [ ] Health check configuration added
- [ ] Health check endpoint mapped
- [ ] Endpoint returns "Healthy"
- [ ] Build succeeds
- [ ] Committed to git
## Notes
- Health check endpoint: /health
- Monitors PostgreSQL connection
- Ready for Kubernetes liveness/readiness probes

View File

@ -1,74 +0,0 @@
# Task 8: Create Dockerfile
## Task Description
**Files:**
- Create: `src/Fengling.AuthService/Dockerfile`
- Create: `src/Fengling.AuthService/.dockerignore`
## Implementation Steps
### Step 1: Create Dockerfile
Create: `src/Fengling.AuthService/Dockerfile`
```dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["Fengling.AuthService.csproj", "./"]
RUN dotnet restore "Fengling.AuthService.csproj"
COPY . .
WORKDIR "/src"
RUN dotnet build "Fengling.AuthService.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Fengling.AuthService.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Fengling.AuthService.dll"]
```
### Step 2: Create .dockerignore
Create: `src/Fengling.AuthService/.dockerignore`
```
bin/
obj/
Dockerfile
.dockerignore
*.md
```
### Step 3: Commit
```bash
git add src/Fengling.AuthService/Dockerfile src/Fengling.AuthService/.dockerignore
git commit -m "feat(auth): add Dockerfile for containerization"
```
## Context
This task adds Docker support for containerization. The multi-stage Dockerfile builds the project in a SDK container and copies only the runtime to a lightweight runtime container.
**Tech Stack**: Docker, .NET 10.0
## Verification
- [ ] Dockerfile created with multi-stage build
- [ ] .dockerignore created
- [ ] Dockerfile uses .NET 10.0
- [ ] Exposes port 80
- [ ] Committed to git
## Notes
- Multi-stage build reduces image size
- Only runtime container in final image
- Ready for Kubernetes deployment

View File

@ -1,176 +0,0 @@
# Task 10: Add OAuth Client Management
## Task Description
**Files:**
- Create: `src/Fengling.AuthService/Models/OAuthApplication.cs`
- Create: `src/Fengling.AuthService/Controllers/OAuthClientsController.cs`
- Modify: `src/Fengling.AuthService/Data/ApplicationDbContext.cs`
## Implementation Steps
### Step 1: Create OAuthApplication model
Create: `src/Fengling.AuthService/Models/OAuthApplication.cs`
```csharp
namespace Fengling.AuthService.Models;
public class OAuthApplication
{
public long Id { get; set; }
public string ClientId { get; set; } = string.Empty;
public string? ClientSecret { get; set; }
public string DisplayName { get; set; } = string.Empty;
public string[] RedirectUris { get; set; } = Array.Empty<string>();
public string[] PostLogoutRedirectUris { get; set; } = Array.Empty<string>();
public string[] Scopes { get; set; } = Array.Empty<string>();
public string[] GrantTypes { get; set; } = Array.Empty<string>();
public string ClientType { get; set; } = "public";
public string ConsentType { get; set; } = "implicit";
public string Status { get; set; } = "active";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}
```
### Step 2: Update ApplicationDbContext
Edit: `src/Fengling.AuthService/Data/ApplicationDbContext.cs`
Add to context:
```csharp
public DbSet<OAuthApplication> OAuthApplications { get; set; }
```
Add to OnModelCreating:
```csharp
builder.Entity<OAuthApplication>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.ClientId).IsUnique();
entity.Property(e => e.ClientId).HasMaxLength(100);
entity.Property(e => e.ClientSecret).HasMaxLength(200);
entity.Property(e => e.DisplayName).HasMaxLength(100);
entity.Property(e => e.ClientType).HasMaxLength(20);
entity.Property(e => e.ConsentType).HasMaxLength(20);
entity.Property(e => e.Status).HasMaxLength(20);
});
```
### Step 3: Add migration
Run:
```bash
dotnet ef migrations add AddOAuthApplications
dotnet ef database update
```
### Step 4: Create OAuthClientsController
Create: `src/Fengling.AuthService/Controllers/OAuthClientsController.cs`
```csharp
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
public class OAuthClientsController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly ILogger<OAuthClientsController> _logger;
public OAuthClientsController(
ApplicationDbContext context,
ILogger<OAuthClientsController> logger)
{
_context = context;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<OAuthApplication>>> GetClients()
{
return await _context.OAuthApplications.ToListAsync();
}
[HttpGet("{id}")]
public async Task<ActionResult<OAuthApplication>> GetClient(long id)
{
var client = await _context.OAuthApplications.FindAsync(id);
if (client == null)
{
return NotFound();
}
return client;
}
[HttpPost]
public async Task<ActionResult<OAuthApplication>> CreateClient(OAuthApplication application)
{
_context.OAuthApplications.Add(application);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetClient), new { id = application.Id }, application);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateClient(long id, OAuthApplication application)
{
if (id != application.Id)
{
return BadRequest();
}
_context.Entry(application).State = EntityState.Modified;
await _context.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteClient(long id)
{
var client = await _context.OAuthApplications.FindAsync(id);
if (client == null)
{
return NotFound();
}
_context.OAuthApplications.Remove(client);
await _context.SaveChangesAsync();
return NoContent();
}
}
```
### Step 5: Commit
```bash
git add src/Fengling.AuthService/Models/ src/Fengling.AuthService/Controllers/ src/Fengling.AuthService/Data/
git commit -m "feat(auth): add OAuth client management API"
```
## Context
This task adds OAuth client management functionality for managing OAuth applications. This will be used by Fengling.Console to register and manage clients.
**Tech Stack**: EF Core, ASP.NET Core Controllers
## Verification
- [ ] OAuthApplication model created
- [ ] DbSet added to ApplicationDbContext
- [ ] Migration generated and applied
- [ ] CRUD API endpoints created
- [ ] Build succeeds
- [ ] Committed to git
## Notes
- Clients can be registered through this API
- Fengling.Console will be pre-registered in Task 11
- Status field enables client enable/disable

View File

@ -1,69 +0,0 @@
# Task 11: Pre-register Fengling.Console as OAuth Client
## Task Description
**Files:**
- Modify: `src/Fengling.AuthService/Data/SeedData.cs`
## Implementation Steps
### Step 1: Add Fengling.Console registration to SeedData
Edit: `src/Fengling.AuthService/Data/SeedData.cs`
Add after existing seed data:
```csharp
// Register Fengling.Console as OAuth client
var consoleClient = await context.OAuthApplications
.FirstOrDefaultAsync(c => c.ClientId == "fengling-console");
if (consoleClient == null)
{
consoleClient = new OAuthApplication
{
ClientId = "fengling-console",
ClientSecret = "console-secret-change-in-production",
DisplayName = "Fengling 运管中心",
RedirectUris = new[] { "http://console.fengling.local/auth/callback" },
PostLogoutRedirectUris = new[] { "http://console.fengling.local/" },
Scopes = new[] { "api", "offline_access" },
GrantTypes = new[] { "authorization_code", "refresh_token" },
ClientType = "confidential",
ConsentType = "implicit",
Status = "active",
CreatedAt = DateTime.UtcNow
};
context.OAuthApplications.Add(consoleClient);
await context.SaveChangesAsync();
}
```
### Step 2: Commit
```bash
git add src/Fengling.AuthService/Data/SeedData.cs
git commit -m "feat(auth): pre-register Fengling.Console as OAuth client"
```
## Context
This task pre-registers Fengling.Console as an OAuth client in the seed data. This allows the console to use OAuth2 authorization code flow for authentication.
**OAuth Client Configuration:**
- ClientId: `fengling-console`
- Redirect URI: `http://console.fengling.local/auth/callback`
- Scopes: `api`, `offline_access`
- Grant Types: `authorization_code`, `refresh_token`
## Verification
- [ ] Fengling.Console client added to seed data
- [ ] Client configured with correct redirect URIs
- [ ] Client has required scopes and grant types
- [ ] Build succeeds
- [ ] Committed to git
## Notes
- Client secret should be changed in production
- Redirect URI matches Fengling.Console domain
- Client will be created on first application startup

View File

@ -1,101 +0,0 @@
# Task 12: Create Fengling.Console Project Structure
## Task Description
**Files:**
- Create: `src/Fengling.Console/Fengling.Console.csproj`
- Create: `src/Fengling.Console/Program.cs`
- Create: `src/Fengling.Console/appsettings.json`
## Implementation Steps
### Step 1: Create project
Run:
```bash
cd /Users/movingsam/Fengling.Refactory.Buiding/src
dotnet new webapi -n Fengling.Console -o Fengling.Console
```
### Step 2: Update project file with dependencies
Edit: `src/Fengling.Console/Fengling.Console.csproj`
```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="OpenTelemetry" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.2" />
</ItemGroup>
</Project>
```
### Step 3: Create appsettings.json
Edit: `src/Fengling.Console/appsettings.json`
```json
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
},
"AuthService": {
"BaseUrl": "http://auth.fengling.local",
"ClientId": "fengling-console",
"ClientSecret": "console-secret-change-in-production"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
```
### Step 4: Commit
```bash
git add src/Fengling.Console/
git commit -m "feat(console): create console backend project structure"
```
## Context
This task creates the Fengling.Console backend project. This will be a unified management API that includes gateway management, OAuth client management, and user management proxy.
**Tech Stack**: .NET 10.0, EF Core 10.0, PostgreSQL, Serilog, OpenTelemetry
## Verification
- [ ] Project created with webapi template
- [ ] Target framework set to net10.0
- [ ] All packages added
- [ ] appsettings.json configured
- [ ] Build succeeds
- [ ] Committed to git
## Notes
- Shares database with YarpGateway
- Communicates with AuthService via OAuth2
- Will contain migrated gateway management APIs

View File

@ -1,152 +0,0 @@
# Task 12: Create Fengling.Console.Web Frontend Project
## Task Description
**Files:**
- Create: `src/Fengling.Console.Web/` (Vue 3 + Vite project)
- Create: `src/Fengling.Console.Web/.env.development`
- Create: `src/Fengling.Console.Web/.env.production`
- Create: `src/Fengling.Console.Web/vite.config.ts`
- Create: `src/Fengling.Console.Web/src/api/auth.ts`
- Create: `src/Fengling.Console.Web/src/api/gateway.ts`
- Create: `src/Fengling.Console.Web/src/stores/auth.ts`
- Create: `src/Fengling.Console.Web/src/router/index.ts`
- Create: `src/Fengling.Console.Web/src/views/Auth/Login.vue`
- Create: `src/Fengling.Console.Web/src/views/Auth/Callback.vue`
- Create: `src/Fengling.Console.Web/src/views/Gateway/Dashboard.vue`
- Create: `src/Fengling.Console.Web/src/views/OAuth/ClientList.vue`
- Create: `src/Fengling.Console.Web/src/views/Users/UserList.vue`
## Implementation Steps
### Step 1: Create Vue project
Run:
```bash
cd /Users/movingsam/Fengling.Refactory.Buiding/src
npm create vite@latest Fengling.Console.Web -- --template vue-ts
```
### Step 2: Install dependencies
Run:
```bash
cd Fengling.Console.Web
npm install
npm install element-plus @element-plus/icons-vue pinia vue-router axios
```
### Step 3: Configure environment variables
Create: `src/Fengling.Console.Web/.env.development`
```env
VITE_AUTH_SERVICE_URL=http://localhost:5000
VITE_GATEWAY_SERVICE_URL=http://localhost:5001
VITE_CLIENT_ID=fengling-console
VITE_REDIRECT_URI=http://localhost:5173/auth/callback
```
Create: `src/Fengling.Console.Web/.env.production`
```env
VITE_AUTH_SERVICE_URL=https://auth.fengling.local
VITE_GATEWAY_SERVICE_URL=https://gateway.fengling.local
VITE_CLIENT_ID=fengling-console
VITE_REDIRECT_URI=https://console.fengling.local/auth/callback
```
### Step 4: Configure Vite
Create: `src/Fengling.Console.Web/vite.config.ts`
```typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig(({ mode }) => ({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
port: 5173,
proxy: {
'/api/auth': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/auth/, '')
},
'/api/gateway': {
target: 'http://localhost:5001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/gateway/, '/api')
}
}
}
}))
```
### Step 5: Create API modules
Create: `src/Fengling.Console.Web/src/api/auth.ts` - AuthService API封装
Create: `src/Fengling.Console.Web/src/api/gateway.ts` - Gateway API封装
### Step 6: Create store
Create: `src/Fengling.Console.Web/src/stores/auth.ts` - Pinia store for auth
### Step 7: Create router
Create: `src/Fengling.Console.Web/src/router/index.ts` - Vue Router配置
### Step 8: Create views
Create authentication views:
- `src/Fengling.Console.Web/src/views/Auth/Login.vue`
- `src/Fengling.Console.Web/src/views/Auth/Callback.vue`
Create gateway management views:
- `src/Fengling.Console.Web/src/views/Gateway/Dashboard.vue`
Create OAuth management views:
- `src/Fengling.Console.Web/src/views/OAuth/ClientList.vue`
Create user management views:
- `src/Fengling.Console.Web/src/views/Users/UserList.vue`
### Step 9: Commit
```bash
git add src/Fengling.Console.Web/
git commit -m "feat(console): create Vue 3 frontend project with OAuth2 and basic modules"
```
## Context
This task creates the Fengling.Console.Web frontend project using Vue 3 + Vite. The frontend directly calls AuthService and YarpGateway APIs through Vite proxy configuration.
**Tech Stack**: Vue 3.4, TypeScript, Vite 5.4, Element Plus, Pinia, Vue Router 4, Axios
## Verification
- [ ] Vue 3 project created with Vite
- [ ] Dependencies installed (Element Plus, Pinia, Vue Router, Axios)
- [ ] Environment variables configured
- [ ] Vite proxy configured for auth and gateway APIs
- [ ] API modules created
- [ ] Auth store created
- [ ] Router configured
- [ ] Authentication views created (Login, Callback)
- [ ] Dashboard view created
- [ ] OAuth Client management view created
- [ ] User management view created
- [ ] Build succeeds
- [ ] Committed to git
## Notes
- OAuth2 authorization code flow ready but AuthService endpoint not yet implemented
- Password flow login implemented
- Gateway management views can be migrated from YarpGateway.Admin
- Token refresh interceptor to be added

View File

@ -1,398 +0,0 @@
# YARP Gateway 测试指南
## 1. 数据库准备
### 使用EF Core Migrations初始化数据库
项目使用 EF Core 管理数据库,已生成迁移脚本位于 `sql/init.sql`
```bash
# 连接到PostgreSQL并执行迁移脚本
psql -h 192.168.100.10 -U postgres -d fengling_gateway -f sql/init.sql
```
如果数据库不存在,先创建数据库:
```bash
psql -h 192.168.100.10 -U postgres -c "CREATE DATABASE fengling_gateway;"
```
### 验证数据
```bash
psql -h 192.168.100.10 -U postgres -d fengling_gateway -c "\dt"
```
应该看到3个表
- `Tenants`
- `TenantRoutes`
- `ServiceInstances`
## 2. 启动网关
```bash
cd /Users/movingsam/Fengling.Refactory.Buiding/src/YarpGateway
dotnet run
```
网关将在 `http://localhost:8080` 启动。
## 3. 测试API接口
### 3.1 创建租户
```bash
curl -X POST http://localhost:8080/api/gateway/tenants \
-H "Content-Type: application/json" \
-d '{
"tenantCode": "customerA",
"tenantName": "客户A"
}'
```
响应示例:
```json
{
"id": 1738377721234,
"tenantCode": "customerA",
"tenantName": "客户A",
"status": 1,
"createdBy": null,
"createdTime": "2026-02-01T20:08:41.234Z",
"updatedBy": null,
"updatedTime": null,
"isDeleted": false,
"version": 0
}
```
### 3.2 查看所有租户
```bash
curl http://localhost:8080/api/gateway/tenants
```
### 3.3 为租户创建路由
```bash
curl -X POST http://localhost:8080/api/gateway/tenants/customerA/routes \
-H "Content-Type: application/json" \
-d '{
"serviceName": "product",
"pathPattern": "/api/product/{**catch-all}"
}'
```
响应示例:
```json
{
"id": 1738377722345,
"tenantCode": "customerA",
"serviceName": "product",
"clusterId": "customerA-product",
"pathPattern": "/api/product/{**catch-all}",
"priority": 0,
"status": 1
}
```
### 3.4 查看租户路由
```bash
curl http://localhost:8080/api/gateway/tenants/customerA/routes
```
### 3.5 添加服务实例
```bash
curl -X POST http://localhost:8080/api/gateway/clusters/customerA-product/instances \
-H "Content-Type: application/json" \
-d '{
"destinationId": "product-1",
"address": "http://localhost:8001",
"weight": 1
}'
```
### 3.6 查看Cluster实例
```bash
curl http://localhost:8080/api/gateway/clusters/customerA-product/instances
```
### 3.7 重新加载配置
```bash
curl -X POST http://localhost:8080/api/gateway/reload
```
响应:
```json
{
"message": "Config reloaded successfully"
}
```
## 4. JWT测试
### 4.1 生成测试JWT
使用 https://jwt.io/ 生成测试JWT
**Header**:
```json
{
"alg": "HS256",
"typ": "JWT"
}
```
**Payload**:
```json
{
"tenant": "customerA",
"sub": "123456",
"unique_name": "张三",
"role": ["admin", "user"],
"exp": 1738369200
}
```
**Secret** (示例密钥):
```
your-secret-key-at-least-32-bytes-long
```
### 4.2 测试JWT解析
```bash
JWT_TOKEN="your-generated-jwt-token"
curl -X GET http://localhost:8080/api/product/list \
-H "Authorization: Bearer $JWT_TOKEN"
```
### 4.3 验证Header传递
创建一个测试服务来接收请求:
**创建测试服务** (test-service.js):
```javascript
const http = require('http');
const server = http.createServer((req, res) => {
console.log('收到请求:');
console.log('URL:', req.url);
console.log('Headers:', req.headers);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: '请求成功',
tenantId: req.headers['x-tenant-id'],
userId: req.headers['x-user-id'],
userName: req.headers['x-user-name'],
roles: req.headers['x-roles']
}));
});
server.listen(8001, () => {
console.log('测试服务运行在 http://localhost:8001');
});
```
**启动测试服务**:
```bash
node test-service.js
```
## 5. 负载均衡测试
### 5.1 添加多个服务实例
```bash
# 实例1
curl -X POST http://localhost:8080/api/gateway/clusters/customerA-product/instances \
-H "Content-Type: application/json" \
-d '{
"destinationId": "product-1",
"address": "http://localhost:8001",
"weight": 10
}'
# 实例2
curl -X POST http://localhost:8080/api/gateway/clusters/customerA-product/instances \
-H "Content-Type: application/json" \
-d '{
"destinationId": "product-2",
"address": "http://localhost:8002",
"weight": 5
}'
```
### 5.2 并发测试
```bash
for i in {1..20}; do
curl -X GET http://localhost:8080/api/product/list \
-H "Authorization: Bearer $JWT_TOKEN" &
done
wait
```
观察日志输出,验证请求是否按权重分配到不同实例。
## 6. 日志查看
### 查看网关日志
控制台会输出实时日志,包括:
- JWT解析日志
- 路由选择日志
- 负载均衡选择日志
### 日志文件位置
`/Users/movingsam/Fengling.Refactory.Buiding/src/YarpGateway/logs/gateway-YYYYMMDD.log`
## 7. 故障排查
### 问题1: 无法连接数据库
**测试连接**:
```bash
psql -h 192.168.100.10 -U postgres -d fengling_gateway
```
**检查配置**:
- 确认 `appsettings.json` 中的连接字符串正确
- 确认 PostgreSQL 已启动并可访问
### 问题2: 路由404
**检查租户配置**:
```bash
curl http://localhost:8080/api/gateway/tenants
```
**检查路由配置**:
```bash
curl http://localhost:8080/api/gateway/tenants/customerA/routes
```
**检查实例配置**:
```bash
curl http://localhost:8080/api/gateway/clusters/customerA-product/instances
```
### 问题3: JWT解析失败
**验证JWT格式**:
```bash
echo "your.jwt.token" | cut -d'.' -f2 | base64 -d
```
**检查日志中的错误信息**
### 问题4: 数据库迁移失败
**手动执行SQL脚本**:
```bash
psql -h 192.168.100.10 -U postgres -d fengling_gateway -f sql/init.sql
```
**检查迁移历史**:
```sql
SELECT * FROM "__EFMigrationsHistory";
```
## 8. 使用Docker Compose
如果使用Docker Compose部署
```bash
cd /Users/movingsam/Fengling.Refactory.Buiding/docker
docker-compose up -d
```
查看日志:
```bash
docker-compose logs -f gateway
```
## 9. 清理
### 停止网关
`Ctrl+C` 停止 `dotnet run`
### 删除数据库(如果需要)
```bash
psql -h 192.168.100.10 -U postgres -c "DROP DATABASE fengling_gateway;"
```
## 10. 创建示例数据
### 创建完整示例数据
```bash
# 1. 创建租户
curl -X POST http://localhost:8080/api/gateway/tenants \
-H "Content-Type: application/json" \
-d '{"tenantCode": "customerB", "tenantName": "客户B"}'
# 2. 创建订单服务路由
curl -X POST http://localhost:8080/api/gateway/tenants/customerA/routes \
-H "Content-Type: application/json" \
-d '{"serviceName": "order", "pathPattern": "/api/order/{**catch-all}"}'
# 3. 为客户B创建产品服务
curl -X POST http://localhost:8080/api/gateway/tenants/customerB/routes \
-H "Content-Type: application/json" \
-d '{"serviceName": "product", "pathPattern": "/api/product/{**catch-all}"}'
# 4. 为客户B产品服务添加实例
curl -X POST http://localhost:8080/api/gateway/clusters/customerB-product/instances \
-H "Content-Type: application/json" \
-d '{"destinationId": "product-1", "address": "http://localhost:8003", "weight": 1}'
```
### 验证配置
```bash
# 查看所有租户
curl http://localhost:8080/api/gateway/tenants | jq
# 查看客户A的所有路由
curl http://localhost:8080/api/gateway/tenants/customerA/routes | jq
# 查看客户A产品服务的所有实例
curl http://localhost:8080/api/gateway/clusters/customerA-product/instances | jq
```
## 11. EF Core Migrations管理
### 添加新迁移
```bash
cd /Users/movingsam/Fengling.Refactory.Buiding/src/YarpGateway
dotnet ef migrations add AddNewFeature
```
### 生成SQL脚本
```bash
dotnet ef migrations script --context GatewayDbContext --output ../sql/migration_$(date +%Y%m%d).sql
```
### 应用迁移到数据库
```bash
dotnet ef database update --context GatewayDbContext
```
### 回滚迁移
```bash
dotnet ef database update 20260201120312_InitialCreate --context GatewayDbContext
```

1421
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
{
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"axios": "^1.13.4",
"element-plus": "^2.13.2",
"pinia": "^3.0.4",
"vue-router": "^5.0.1"
}
}

View File

@ -1,89 +0,0 @@
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
"MigrationId" character varying(150) NOT NULL,
"ProductVersion" character varying(32) NOT NULL,
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
);
START TRANSACTION;
CREATE TABLE "ServiceInstances" (
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
"ClusterId" character varying(100) NOT NULL,
"DestinationId" character varying(100) NOT NULL,
"Address" character varying(200) NOT NULL,
"Health" integer NOT NULL,
"Weight" integer NOT NULL,
"Status" integer NOT NULL,
"CreatedBy" bigint,
"CreatedTime" timestamp with time zone NOT NULL,
"UpdatedBy" bigint,
"UpdatedTime" timestamp with time zone,
"IsDeleted" boolean NOT NULL,
"Version" integer NOT NULL,
CONSTRAINT "PK_ServiceInstances" PRIMARY KEY ("Id")
);
CREATE TABLE "Tenants" (
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
"TenantCode" character varying(50) NOT NULL,
"TenantName" character varying(100) NOT NULL,
"Status" integer NOT NULL,
"CreatedBy" bigint,
"CreatedTime" timestamp with time zone NOT NULL,
"UpdatedBy" bigint,
"UpdatedTime" timestamp with time zone,
"IsDeleted" boolean NOT NULL,
"Version" integer NOT NULL,
CONSTRAINT "PK_Tenants" PRIMARY KEY ("Id"),
CONSTRAINT "AK_Tenants_TenantCode" UNIQUE ("TenantCode")
);
CREATE TABLE "TenantRoutes" (
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
"TenantCode" character varying(50) NOT NULL,
"ServiceName" character varying(100) NOT NULL,
"ClusterId" character varying(100) NOT NULL,
"PathPattern" character varying(200) NOT NULL,
"Priority" integer NOT NULL,
"Status" integer NOT NULL,
"CreatedBy" bigint,
"CreatedTime" timestamp with time zone NOT NULL,
"UpdatedBy" bigint,
"UpdatedTime" timestamp with time zone,
"IsDeleted" boolean NOT NULL,
"Version" integer NOT NULL,
CONSTRAINT "PK_TenantRoutes" PRIMARY KEY ("Id"),
CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode" FOREIGN KEY ("TenantCode") REFERENCES "Tenants" ("TenantCode") ON DELETE RESTRICT
);
CREATE UNIQUE INDEX "IX_ServiceInstances_ClusterId_DestinationId" ON "ServiceInstances" ("ClusterId", "DestinationId");
CREATE INDEX "IX_ServiceInstances_Health" ON "ServiceInstances" ("Health");
CREATE INDEX "IX_TenantRoutes_ClusterId" ON "TenantRoutes" ("ClusterId");
CREATE UNIQUE INDEX "IX_TenantRoutes_TenantCode_ServiceName" ON "TenantRoutes" ("TenantCode", "ServiceName");
CREATE UNIQUE INDEX "IX_Tenants_TenantCode" ON "Tenants" ("TenantCode");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260201120312_InitialCreate', '9.0.0');
ALTER TABLE "TenantRoutes" DROP CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode";
ALTER TABLE "Tenants" DROP CONSTRAINT "AK_Tenants_TenantCode";
DROP INDEX "IX_TenantRoutes_TenantCode_ServiceName";
ALTER TABLE "TenantRoutes" ADD "IsGlobal" boolean NOT NULL DEFAULT FALSE;
CREATE INDEX "IX_TenantRoutes_ServiceName" ON "TenantRoutes" ("ServiceName");
CREATE INDEX "IX_TenantRoutes_ServiceName_IsGlobal_Status" ON "TenantRoutes" ("ServiceName", "IsGlobal", "Status");
CREATE INDEX "IX_TenantRoutes_TenantCode" ON "TenantRoutes" ("TenantCode");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260201133826_AddIsGlobalToTenantRoute', '9.0.0');
COMMIT;

View File

@ -1,5 +0,0 @@
bin/
obj/
Dockerfile
.dockerignore
*.md

View File

@ -1,53 +0,0 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.DependencyInjection;
namespace Fengling.AuthService.Configuration;
public static class OpenIddictSetup
{
public static IServiceCollection AddOpenIddictConfiguration(
this IServiceCollection services,
IConfiguration configuration
)
{
var isTesting = configuration.GetValue<bool>("Testing", false);
var builder = services.AddOpenIddict();
builder.AddCore(options =>
{
options.UseEntityFrameworkCore().UseDbContext<Data.ApplicationDbContext>();
});
if (!isTesting)
{
builder.AddServer(options =>
{
options.SetIssuer(configuration["OpenIddict:Issuer"] ?? "https://auth.fengling.local");
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.AllowAuthorizationCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.RequireProofKeyForCodeExchange();
options.RegisterScopes("api", "offline_access");
});
}
builder.AddValidation(options =>
{
options.UseLocalServer();
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
return services;
}
}

View File

@ -1,158 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class AccessLogsController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly ILogger<AccessLogsController> _logger;
public AccessLogsController(
ApplicationDbContext context,
ILogger<AccessLogsController> logger)
{
_context = context;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<object>> GetAccessLogs(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? userName = null,
[FromQuery] string? tenantId = null,
[FromQuery] string? action = null,
[FromQuery] string? status = null,
[FromQuery] DateTime? startTime = null,
[FromQuery] DateTime? endTime = null)
{
var query = _context.AccessLogs.AsQueryable();
if (!string.IsNullOrEmpty(userName))
{
query = query.Where(l => l.UserName != null && l.UserName.Contains(userName));
}
if (!string.IsNullOrEmpty(tenantId))
{
query = query.Where(l => l.TenantId == tenantId);
}
if (!string.IsNullOrEmpty(action))
{
query = query.Where(l => l.Action == action);
}
if (!string.IsNullOrEmpty(status))
{
query = query.Where(l => l.Status == status);
}
if (startTime.HasValue)
{
query = query.Where(l => l.CreatedAt >= startTime.Value);
}
if (endTime.HasValue)
{
query = query.Where(l => l.CreatedAt <= endTime.Value);
}
var totalCount = await query.CountAsync();
var logs = await query
.OrderByDescending(l => l.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var result = logs.Select(l => new
{
id = l.Id,
userName = l.UserName,
tenantId = l.TenantId,
action = l.Action,
resource = l.Resource,
method = l.Method,
ipAddress = l.IpAddress,
userAgent = l.UserAgent,
status = l.Status,
duration = l.Duration,
requestData = l.RequestData,
responseData = l.ResponseData,
errorMessage = l.ErrorMessage,
createdAt = l.CreatedAt,
});
return Ok(new
{
items = result,
totalCount,
page,
pageSize
});
}
[HttpGet("export")]
public async Task<IActionResult> ExportAccessLogs(
[FromQuery] string? userName = null,
[FromQuery] string? tenantId = null,
[FromQuery] string? action = null,
[FromQuery] string? status = null,
[FromQuery] DateTime? startTime = null,
[FromQuery] DateTime? endTime = null)
{
var query = _context.AccessLogs.AsQueryable();
if (!string.IsNullOrEmpty(userName))
{
query = query.Where(l => l.UserName != null && l.UserName.Contains(userName));
}
if (!string.IsNullOrEmpty(tenantId))
{
query = query.Where(l => l.TenantId == tenantId);
}
if (!string.IsNullOrEmpty(action))
{
query = query.Where(l => l.Action == action);
}
if (!string.IsNullOrEmpty(status))
{
query = query.Where(l => l.Status == status);
}
if (startTime.HasValue)
{
query = query.Where(l => l.CreatedAt >= startTime.Value);
}
if (endTime.HasValue)
{
query = query.Where(l => l.CreatedAt <= endTime.Value);
}
var logs = await query
.OrderByDescending(l => l.CreatedAt)
.Take(10000)
.ToListAsync();
var csv = new System.Text.StringBuilder();
csv.AppendLine("ID,UserName,TenantId,Action,Resource,Method,IpAddress,UserAgent,Status,Duration,CreatedAt");
foreach (var log in logs)
{
csv.AppendLine($"{log.Id},{log.UserName},{log.TenantId},{log.Action},{log.Resource},{log.Method},{log.IpAddress},\"{log.UserAgent}\",{log.Status},{log.Duration},{log.CreatedAt:yyyy-MM-dd HH:mm:ss}");
}
return File(System.Text.Encoding.UTF8.GetBytes(csv.ToString()), "text/csv", $"access-logs-{DateTime.UtcNow:yyyyMMdd}.csv");
}
}

View File

@ -1,119 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Fengling.AuthService.ViewModels;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace Fengling.AuthService.Controllers;
[Route("account")]
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<AccountController> _logger;
public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<AccountController> logger)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
[HttpGet("login")]
public IActionResult Login(string returnUrl = "/")
{
return View(new LoginInputModel { ReturnUrl = returnUrl });
}
[HttpPost("login")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.FindByNameAsync(model.Username);
if (user == null || user.IsDeleted)
{
ModelState.AddModelError(string.Empty, "用户名或密码错误");
return View(model);
}
var result = await _signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, false);
if (!result.Succeeded)
{
if (result.IsLockedOut)
{
ModelState.AddModelError(string.Empty, "账号已被锁定");
}
else
{
ModelState.AddModelError(string.Empty, "用户名或密码错误");
}
return View(model);
}
return LocalRedirect(model.ReturnUrl);
}
[HttpGet("register")]
public IActionResult Register(string returnUrl = "/")
{
return View(new RegisterViewModel { ReturnUrl = returnUrl });
}
[HttpPost("register")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = new ApplicationUser
{
UserName = model.Username,
Email = model.Email,
NormalizedUserName = model.Username.ToUpper(),
NormalizedEmail = model.Email.ToUpper()
};
var result = await _userManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return View(model);
}
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(model.ReturnUrl);
}
[HttpGet("profile")]
[HttpGet("settings")]
[HttpGet("logout")]
public IActionResult NotImplemented()
{
return RedirectToAction("Index", "Dashboard");
}
[HttpPost("logout")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LogoutPost()
{
await _signInManager.SignOutAsync();
return Redirect("/");
}
}

View File

@ -1,159 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class AuditLogsController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly ILogger<AuditLogsController> _logger;
public AuditLogsController(
ApplicationDbContext context,
ILogger<AuditLogsController> logger)
{
_context = context;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<object>> GetAuditLogs(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? operatorName = null,
[FromQuery] string? tenantId = null,
[FromQuery] string? operation = null,
[FromQuery] string? action = null,
[FromQuery] DateTime? startTime = null,
[FromQuery] DateTime? endTime = null)
{
var query = _context.AuditLogs.AsQueryable();
if (!string.IsNullOrEmpty(operatorName))
{
query = query.Where(l => l.Operator.Contains(operatorName));
}
if (!string.IsNullOrEmpty(tenantId))
{
query = query.Where(l => l.TenantId == tenantId);
}
if (!string.IsNullOrEmpty(operation))
{
query = query.Where(l => l.Operation == operation);
}
if (!string.IsNullOrEmpty(action))
{
query = query.Where(l => l.Action == action);
}
if (startTime.HasValue)
{
query = query.Where(l => l.CreatedAt >= startTime.Value);
}
if (endTime.HasValue)
{
query = query.Where(l => l.CreatedAt <= endTime.Value);
}
var totalCount = await query.CountAsync();
var logs = await query
.OrderByDescending(l => l.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var result = logs.Select(l => new
{
id = l.Id,
@operator = l.Operator,
tenantId = l.TenantId,
operation = l.Operation,
action = l.Action,
targetType = l.TargetType,
targetId = l.TargetId,
targetName = l.TargetName,
ipAddress = l.IpAddress,
description = l.Description,
oldValue = l.OldValue,
newValue = l.NewValue,
errorMessage = l.ErrorMessage,
status = l.Status,
createdAt = l.CreatedAt,
});
return Ok(new
{
items = result,
totalCount,
page,
pageSize
});
}
[HttpGet("export")]
public async Task<IActionResult> ExportAuditLogs(
[FromQuery] string? operatorName = null,
[FromQuery] string? tenantId = null,
[FromQuery] string? operation = null,
[FromQuery] string? action = null,
[FromQuery] DateTime? startTime = null,
[FromQuery] DateTime? endTime = null)
{
var query = _context.AuditLogs.AsQueryable();
if (!string.IsNullOrEmpty(operatorName))
{
query = query.Where(l => l.Operator.Contains(operatorName));
}
if (!string.IsNullOrEmpty(tenantId))
{
query = query.Where(l => l.TenantId == tenantId);
}
if (!string.IsNullOrEmpty(operation))
{
query = query.Where(l => l.Operation == operation);
}
if (!string.IsNullOrEmpty(action))
{
query = query.Where(l => l.Action == action);
}
if (startTime.HasValue)
{
query = query.Where(l => l.CreatedAt >= startTime.Value);
}
if (endTime.HasValue)
{
query = query.Where(l => l.CreatedAt <= endTime.Value);
}
var logs = await query
.OrderByDescending(l => l.CreatedAt)
.Take(10000)
.ToListAsync();
var csv = new System.Text.StringBuilder();
csv.AppendLine("ID,Operator,TenantId,Operation,Action,TargetType,TargetId,TargetName,IpAddress,Description,Status,CreatedAt");
foreach (var log in logs)
{
csv.AppendLine($"{log.Id},{log.Operator},{log.TenantId},{log.Operation},{log.Action},{log.TargetType},{log.TargetId},{log.TargetName},{log.IpAddress},\"{log.Description}\",{log.Status},{log.CreatedAt:yyyy-MM-dd HH:mm:ss}");
}
return File(System.Text.Encoding.UTF8.GetBytes(csv.ToString()), "text/csv", $"audit-logs-{DateTime.UtcNow:yyyyMMdd}.csv");
}
}

View File

@ -1,217 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using System.Security.Claims;
using Fengling.AuthService.ViewModels;
using Microsoft.AspNetCore;
using Microsoft.Extensions.Primitives;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("connect")]
public class AuthorizationController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
ILogger<AuthorizationController> logger)
: Controller
{
[HttpGet("authorize")]
[HttpPost("authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// If prompt=login was specified by the client application,
// immediately return the user agent to the login page.
if (request.HasPromptValue(OpenIddictConstants.PromptValues.Login))
{
// To avoid endless login -> authorization redirects, the prompt=login flag
// is removed from the authorization request payload before redirecting the user.
var prompt = string.Join(" ", request.GetPromptValues().Remove(OpenIddictConstants.PromptValues.Login));
var parameters = Request.HasFormContentType
? Request.Form.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList()
: Request.Query.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList();
parameters.Add(KeyValuePair.Create(OpenIddictConstants.Parameters.Prompt, new StringValues(prompt)));
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
}
// Retrieve the user principal stored in the authentication cookie.
// If a max_age parameter was provided, ensure that the cookie is not too old.
// If the user principal can't be extracted or the cookie is too old, redirect the user to the login page.
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result is not { Succeeded: true } || (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
DateTimeOffset.UtcNow - result.Properties.IssuedUtc >
TimeSpan.FromSeconds(request.MaxAge.Value)))
{
// If the client application requested promptless authentication,
// return an error indicating that the user is not logged in.
if (request.HasPromptValue(OpenIddictConstants.PromptValues.None))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] =
OpenIddictConstants.Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}!));
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// Retrieve the profile of the logged in user.
var user = await userManager.GetUserAsync(result.Principal) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
// Retrieve the application details from the database.
var application = await applicationManager.FindByClientIdAsync(request.ClientId!) ??
throw new InvalidOperationException(
"Details concerning the calling client application cannot be found.");
// Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await authorizationManager.FindAsync(
subject: await userManager.GetUserIdAsync(user),
client: (await applicationManager.GetIdAsync(application))!,
status: OpenIddictConstants.Statuses.Valid,
type: OpenIddictConstants.AuthorizationTypes.Permanent,
scopes: request.GetScopes()).ToListAsync();
switch (await applicationManager.GetConsentTypeAsync(application))
{
// If the consent is external (e.g when authorizations are granted by a sysadmin),
// immediately return an error if no authorization can be found in the database.
case OpenIddictConstants.ConsentTypes.External when !authorizations.Any():
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] =
OpenIddictConstants.Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}!));
// If the consent is implicit or if an authorization was found,
// return an authorization response without displaying the consent form.
case OpenIddictConstants.ConsentTypes.Implicit:
case OpenIddictConstants.ConsentTypes.External when authorizations.Any():
case OpenIddictConstants.ConsentTypes.Explicit
when authorizations.Any() && !request.HasPromptValue(OpenIddictConstants.PromptValues.Consent):
var principal = await signInManager.CreateUserPrincipalAsync(user);
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(request.GetScopes());
principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
var authorization = authorizations.LastOrDefault();
if (authorization == null)
{
authorization = await authorizationManager.CreateAsync(
principal: principal,
subject: await userManager.GetUserIdAsync(user),
client: (await applicationManager.GetIdAsync(application))!,
type: OpenIddictConstants.AuthorizationTypes.Permanent,
scopes: principal.GetScopes());
}
principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// At this point, no authorization was found in the database and an error must be returned
// if the client application specified prompt=none in the authorization request.
case OpenIddictConstants.ConsentTypes.Explicit when request.HasPromptValue(OpenIddictConstants.PromptValues.None):
case OpenIddictConstants.ConsentTypes.Systematic when request.HasPromptValue(OpenIddictConstants.PromptValues.None):
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] =
OpenIddictConstants.Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"Interactive user consent is required."
}!));
// In every other case, render the consent form.
default:
return View(new AuthorizeViewModel(await applicationManager.GetDisplayNameAsync(application),request.Scope));
}
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case OpenIddictConstants.Claims.Name:
yield return OpenIddictConstants.Destinations.AccessToken;
if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Profile))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
case OpenIddictConstants.Claims.Email:
yield return OpenIddictConstants.Destinations.AccessToken;
if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Email))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
case OpenIddictConstants.Claims.Role:
yield return OpenIddictConstants.Destinations.AccessToken;
if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Roles))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return OpenIddictConstants.Destinations.AccessToken;
yield break;
}
}
}

View File

@ -1,61 +0,0 @@
using Fengling.AuthService.ViewModels;
using Microsoft.AspNetCore.Mvc;
namespace Fengling.AuthService.Controllers;
[Route("dashboard")]
public class DashboardController : Controller
{
private readonly ILogger<DashboardController> _logger;
public DashboardController(ILogger<DashboardController> logger)
{
_logger = logger;
}
[HttpGet("")]
[HttpGet("index")]
public IActionResult Index()
{
if (User.Identity?.IsAuthenticated != true)
{
return RedirectToAction("Login", "Account", new { returnUrl = "/dashboard" });
}
return View("Index", new DashboardViewModel
{
Username = User.Identity?.Name,
Email = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value
});
}
[HttpGet("profile")]
public IActionResult Profile()
{
if (User.Identity?.IsAuthenticated != true)
{
return RedirectToAction("Login", "Account", new { returnUrl = "/dashboard/profile" });
}
return View(new DashboardViewModel
{
Username = User.Identity?.Name,
Email = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value
});
}
[HttpGet("settings")]
public IActionResult Settings()
{
if (User.Identity?.IsAuthenticated != true)
{
return RedirectToAction("Login", "Account", new { returnUrl = "/dashboard/settings" });
}
return View(new DashboardViewModel
{
Username = User.Identity?.Name,
Email = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value
});
}
}

View File

@ -1,71 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("connect")]
public class LogoutController : ControllerBase
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LogoutController> _logger;
public LogoutController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<LogoutController> logger)
{
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
[HttpGet("endsession")]
[HttpPost("endsession")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> EndSession()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("OpenIddict request is null");
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result.Succeeded)
{
await _signInManager.SignOutAsync();
}
if (request.ClientId != null)
{
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application != null)
{
var postLogoutRedirectUri = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
if (!string.IsNullOrEmpty(request.PostLogoutRedirectUri))
{
if (postLogoutRedirectUri.Contains(request.PostLogoutRedirectUri))
{
return Redirect(request.PostLogoutRedirectUri);
}
}
}
}
return Redirect("/");
}
}

View File

@ -1,263 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class OAuthClientsController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly ILogger<OAuthClientsController> _logger;
public OAuthClientsController(
ApplicationDbContext context,
ILogger<OAuthClientsController> logger)
{
_context = context;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<object>> GetClients(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? displayName = null,
[FromQuery] string? clientId = null,
[FromQuery] string? status = null)
{
var query = _context.OAuthApplications.AsQueryable();
if (!string.IsNullOrEmpty(displayName))
{
query = query.Where(c => c.DisplayName.Contains(displayName));
}
if (!string.IsNullOrEmpty(clientId))
{
query = query.Where(c => c.ClientId.Contains(clientId));
}
if (!string.IsNullOrEmpty(status))
{
query = query.Where(c => c.Status == status);
}
var totalCount = await query.CountAsync();
var clients = await query
.OrderByDescending(c => c.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var result = clients.Select(c => new
{
id = c.Id,
clientId = c.ClientId,
displayName = c.DisplayName,
redirectUris = c.RedirectUris,
postLogoutRedirectUris = c.PostLogoutRedirectUris,
scopes = c.Scopes,
grantTypes = c.GrantTypes,
clientType = c.ClientType,
consentType = c.ConsentType,
status = c.Status,
description = c.Description,
createdAt = c.CreatedAt,
});
return Ok(new
{
items = result,
totalCount,
page,
pageSize
});
}
[HttpGet("{id}")]
public async Task<ActionResult<OAuthApplication>> GetClient(long id)
{
var client = await _context.OAuthApplications.FindAsync(id);
if (client == null)
{
return NotFound();
}
return Ok(new
{
id = client.Id,
clientId = client.ClientId,
displayName = client.DisplayName,
redirectUris = client.RedirectUris,
postLogoutRedirectUris = client.PostLogoutRedirectUris,
scopes = client.Scopes,
grantTypes = client.GrantTypes,
clientType = client.ClientType,
consentType = client.ConsentType,
status = client.Status,
description = client.Description,
createdAt = client.CreatedAt,
});
}
[HttpGet("{id}/secret")]
public async Task<ActionResult<object>> GetClientSecret(long id)
{
var client = await _context.OAuthApplications.FindAsync(id);
if (client == null)
{
return NotFound();
}
return Ok(new
{
clientId = client.ClientId,
clientSecret = client.ClientSecret,
});
}
[HttpPost]
public async Task<ActionResult<OAuthApplication>> CreateClient(CreateOAuthClientDto dto)
{
if (await _context.OAuthApplications.AnyAsync(c => c.ClientId == dto.ClientId))
{
return BadRequest(new { message = "Client ID 已存在" });
}
var client = new OAuthApplication
{
ClientId = dto.ClientId,
ClientSecret = dto.ClientSecret,
DisplayName = dto.DisplayName,
RedirectUris = dto.RedirectUris,
PostLogoutRedirectUris = dto.PostLogoutRedirectUris,
Scopes = dto.Scopes,
GrantTypes = dto.GrantTypes,
ClientType = dto.ClientType,
ConsentType = dto.ConsentType,
Status = dto.Status,
Description = dto.Description,
CreatedAt = DateTime.UtcNow,
};
_context.OAuthApplications.Add(client);
await _context.SaveChangesAsync();
await CreateAuditLog("oauth", "create", "OAuthClient", client.Id, client.DisplayName, null, SerializeToJson(dto));
return CreatedAtAction(nameof(GetClient), new { id = client.Id }, client);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateClient(long id, UpdateOAuthClientDto dto)
{
var client = await _context.OAuthApplications.FindAsync(id);
if (client == null)
{
return NotFound();
}
var oldValue = SerializeToJson(client);
client.DisplayName = dto.DisplayName;
client.RedirectUris = dto.RedirectUris;
client.PostLogoutRedirectUris = dto.PostLogoutRedirectUris;
client.Scopes = dto.Scopes;
client.GrantTypes = dto.GrantTypes;
client.ClientType = dto.ClientType;
client.ConsentType = dto.ConsentType;
client.Status = dto.Status;
client.Description = dto.Description;
await _context.SaveChangesAsync();
await CreateAuditLog("oauth", "update", "OAuthClient", client.Id, client.DisplayName, oldValue, SerializeToJson(client));
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteClient(long id)
{
var client = await _context.OAuthApplications.FindAsync(id);
if (client == null)
{
return NotFound();
}
var oldValue = SerializeToJson(client);
_context.OAuthApplications.Remove(client);
await _context.SaveChangesAsync();
await CreateAuditLog("oauth", "delete", "OAuthClient", client.Id, client.DisplayName, oldValue);
return NoContent();
}
private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null)
{
var userName = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "system";
var tenantId = User.FindFirstValue("TenantId");
var log = new AuditLog
{
Operator = userName,
TenantId = tenantId,
Operation = operation,
Action = action,
TargetType = targetType,
TargetId = targetId,
TargetName = targetName,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
Status = "success",
OldValue = oldValue,
NewValue = newValue,
};
_context.AuditLogs.Add(log);
await _context.SaveChangesAsync();
}
private string SerializeToJson(object obj)
{
return System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = false
});
}
}
public class CreateOAuthClientDto
{
public string ClientId { get; set; } = string.Empty;
public string ClientSecret { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string[] RedirectUris { get; set; } = Array.Empty<string>();
public string[] PostLogoutRedirectUris { get; set; } = Array.Empty<string>();
public string[] Scopes { get; set; } = Array.Empty<string>();
public string[] GrantTypes { get; set; } = Array.Empty<string>();
public string ClientType { get; set; } = "confidential";
public string ConsentType { get; set; } = "implicit";
public string Status { get; set; } = "active";
public string? Description { get; set; }
}
public class UpdateOAuthClientDto
{
public string DisplayName { get; set; } = string.Empty;
public string[] RedirectUris { get; set; } = Array.Empty<string>();
public string[] PostLogoutRedirectUris { get; set; } = Array.Empty<string>();
public string[] Scopes { get; set; } = Array.Empty<string>();
public string[] GrantTypes { get; set; } = Array.Empty<string>();
public string ClientType { get; set; } = "confidential";
public string ConsentType { get; set; } = "implicit";
public string Status { get; set; } = "active";
public string? Description { get; set; }
}

View File

@ -1,290 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class RolesController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly RoleManager<ApplicationRole> _roleManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<RolesController> _logger;
public RolesController(
ApplicationDbContext context,
RoleManager<ApplicationRole> roleManager,
UserManager<ApplicationUser> userManager,
ILogger<RolesController> logger)
{
_context = context;
_roleManager = roleManager;
_userManager = userManager;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<object>> GetRoles(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? name = null,
[FromQuery] string? tenantId = null)
{
var query = _context.Roles.AsQueryable();
if (!string.IsNullOrEmpty(name))
{
query = query.Where(r => r.Name != null && r.Name.Contains(name));
}
if (!string.IsNullOrEmpty(tenantId))
{
query = query.Where(r => r.TenantId.ToString() == tenantId);
}
var totalCount = await query.CountAsync();
var roles = await query
.OrderByDescending(r => r.CreatedTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var result = new List<object>();
foreach (var role in roles)
{
var users = await _userManager.GetUsersInRoleAsync(role.Name!);
result.Add(new
{
id = role.Id,
name = role.Name,
displayName = role.DisplayName,
description = role.Description,
tenantId = role.TenantId,
isSystem = role.IsSystem,
permissions = role.Permissions,
userCount = users.Count,
createdAt = role.CreatedTime,
});
}
return Ok(new
{
items = result,
totalCount,
page,
pageSize
});
}
[HttpGet("{id}")]
public async Task<ActionResult<ApplicationRole>> GetRole(long id)
{
var role = await _context.Roles.FindAsync(id);
if (role == null)
{
return NotFound();
}
return Ok(new
{
id = role.Id,
name = role.Name,
displayName = role.DisplayName,
description = role.Description,
tenantId = role.TenantId,
isSystem = role.IsSystem,
permissions = role.Permissions,
createdAt = role.CreatedTime,
});
}
[HttpGet("{id}/users")]
public async Task<ActionResult<List<object>>> GetRoleUsers(long id)
{
var role = await _context.Roles.FindAsync(id);
if (role == null)
{
return NotFound();
}
var users = await _userManager.GetUsersInRoleAsync(role.Name!);
var result = users.Select(async u => new
{
id = u.Id,
userName = u.UserName,
email = u.Email,
realName = u.RealName,
tenantId = u.TenantId,
roles = await _userManager.GetRolesAsync(u),
isActive = !u.LockoutEnabled || u.LockoutEnd == null || u.LockoutEnd < DateTimeOffset.UtcNow,
createdAt = u.CreatedTime,
});
return Ok(await Task.WhenAll(result));
}
[HttpPost]
public async Task<ActionResult<ApplicationRole>> CreateRole(CreateRoleDto dto)
{
var role = new ApplicationRole
{
Name = dto.Name,
DisplayName = dto.DisplayName,
Description = dto.Description,
TenantId = dto.TenantId,
Permissions = dto.Permissions,
IsSystem = false,
CreatedTime = DateTime.UtcNow,
};
var result = await _roleManager.CreateAsync(role);
if (!result.Succeeded)
{
return BadRequest(result.Errors);
}
await CreateAuditLog("role", "create", "Role", role.Id, role.DisplayName, null, SerializeToJson(dto));
return CreatedAtAction(nameof(GetRole), new { id = role.Id }, role);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateRole(long id, UpdateRoleDto dto)
{
var role = await _context.Roles.FindAsync(id);
if (role == null)
{
return NotFound();
}
if (role.IsSystem)
{
return BadRequest("系统角色不能修改");
}
var oldValue = System.Text.Json.JsonSerializer.Serialize(role);
role.DisplayName = dto.DisplayName;
role.Description = dto.Description;
role.Permissions = dto.Permissions;
await _context.SaveChangesAsync();
await CreateAuditLog("role", "update", "Role", role.Id, role.DisplayName, oldValue, System.Text.Json.JsonSerializer.Serialize(role));
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteRole(long id)
{
var role = await _context.Roles.FindAsync(id);
if (role == null)
{
return NotFound();
}
if (role.IsSystem)
{
return BadRequest("系统角色不能删除");
}
var oldValue = System.Text.Json.JsonSerializer.Serialize(role);
var users = await _userManager.GetUsersInRoleAsync(role.Name!);
foreach (var user in users)
{
await _userManager.RemoveFromRoleAsync(user, role.Name!);
}
_context.Roles.Remove(role);
await _context.SaveChangesAsync();
await CreateAuditLog("role", "delete", "Role", role.Id, role.DisplayName, oldValue);
return NoContent();
}
[HttpDelete("{id}/users/{userId}")]
public async Task<IActionResult> RemoveUserFromRole(long id, long userId)
{
var role = await _context.Roles.FindAsync(id);
if (role == null)
{
return NotFound();
}
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null)
{
return NotFound();
}
var result = await _userManager.RemoveFromRoleAsync(user, role.Name!);
if (!result.Succeeded)
{
return BadRequest(result.Errors);
}
await CreateAuditLog("role", "update", "UserRole", null, $"{role.Name} - {user.UserName}");
return NoContent();
}
private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null)
{
var userName = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "system";
var tenantId = User.FindFirstValue("TenantId");
var log = new AuditLog
{
Operator = userName,
TenantId = tenantId,
Operation = operation,
Action = action,
TargetType = targetType,
TargetId = targetId,
TargetName = targetName,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
Status = "success",
OldValue = oldValue,
NewValue = newValue,
};
_context.AuditLogs.Add(log);
await _context.SaveChangesAsync();
}
private string SerializeToJson(object obj)
{
return System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = false
});
}
}
public class CreateRoleDto
{
public string Name { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string? Description { get; set; }
public long? TenantId { get; set; }
public List<string> Permissions { get; set; } = new();
}
public class UpdateRoleDto
{
public string DisplayName { get; set; } = string.Empty;
public string? Description { get; set; }
public List<string> Permissions { get; set; } = new();
}

View File

@ -1,62 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class StatsController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly ILogger<StatsController> _logger;
public StatsController(
ApplicationDbContext context,
ILogger<StatsController> logger)
{
_context = context;
_logger = logger;
}
[HttpGet("dashboard")]
public async Task<ActionResult<object>> GetDashboardStats()
{
var today = DateTime.UtcNow.Date;
var tomorrow = today.AddDays(1);
var userCount = await _context.Users.CountAsync(u => !u.IsDeleted);
var tenantCount = await _context.Tenants.CountAsync(t => !t.IsDeleted);
var oauthClientCount = await _context.OAuthApplications.CountAsync();
var todayAccessCount = await _context.AccessLogs
.CountAsync(l => l.CreatedAt >= today && l.CreatedAt < tomorrow);
return Ok(new
{
userCount,
tenantCount,
oauthClientCount,
todayAccessCount,
});
}
[HttpGet("system")]
public ActionResult<object> GetSystemStats()
{
var uptime = TimeSpan.FromMilliseconds(Environment.TickCount64);
var process = System.Diagnostics.Process.GetCurrentProcess();
return Ok(new
{
uptime = $"{uptime.Days}天 {uptime.Hours}小时 {uptime.Minutes}分钟",
memoryUsed = process.WorkingSet64 / 1024 / 1024,
cpuTime = process.TotalProcessorTime,
machineName = Environment.MachineName,
osVersion = Environment.OSVersion.ToString(),
processorCount = Environment.ProcessorCount,
});
}
}

View File

@ -1,344 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using System.Text.Json;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class TenantsController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<TenantsController> _logger;
public TenantsController(
ApplicationDbContext context,
UserManager<ApplicationUser> userManager,
ILogger<TenantsController> logger)
{
_context = context;
_userManager = userManager;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<object>> GetTenants(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? name = null,
[FromQuery] string? tenantId = null,
[FromQuery] string? status = null)
{
var query = _context.Tenants.AsQueryable();
if (!string.IsNullOrEmpty(name))
{
query = query.Where(t => t.Name.Contains(name));
}
if (!string.IsNullOrEmpty(tenantId))
{
query = query.Where(t => t.TenantId.Contains(tenantId));
}
if (!string.IsNullOrEmpty(status))
{
query = query.Where(t => t.Status == status);
}
var totalCount = await query.CountAsync();
var tenants = await query
.OrderByDescending(t => t.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var result = new List<object>();
foreach (var tenant in tenants)
{
var userCount = await _context.Users.CountAsync(u => u.TenantId == tenant.Id && !u.IsDeleted);
result.Add(new
{
id = tenant.Id,
tenantId = tenant.TenantId,
name = tenant.Name,
contactName = tenant.ContactName,
contactEmail = tenant.ContactEmail,
contactPhone = tenant.ContactPhone,
maxUsers = tenant.MaxUsers,
userCount,
status = tenant.Status,
expiresAt = tenant.ExpiresAt,
description = tenant.Description,
createdAt = tenant.CreatedAt,
});
}
return Ok(new
{
items = result,
totalCount,
page,
pageSize
});
}
[HttpGet("{id}")]
public async Task<ActionResult<Tenant>> GetTenant(long id)
{
var tenant = await _context.Tenants.FindAsync(id);
if (tenant == null)
{
return NotFound();
}
return Ok(new
{
id = tenant.Id,
tenantId = tenant.TenantId,
name = tenant.Name,
contactName = tenant.ContactName,
contactEmail = tenant.ContactEmail,
contactPhone = tenant.ContactPhone,
maxUsers = tenant.MaxUsers,
status = tenant.Status,
expiresAt = tenant.ExpiresAt,
description = tenant.Description,
createdAt = tenant.CreatedAt,
updatedAt = tenant.UpdatedAt,
});
}
[HttpGet("{tenantId}/users")]
public async Task<ActionResult<List<object>>> GetTenantUsers(string tenantId)
{
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.TenantId == tenantId);
if (tenant == null)
{
return NotFound();
}
var users = await _context.Users
.Where(u => u.TenantId == tenant.Id && !u.IsDeleted)
.ToListAsync();
var result = users.Select(async u => new
{
id = u.Id,
userName = u.UserName,
email = u.Email,
realName = u.RealName,
tenantId = u.TenantId,
roles = await _userManager.GetRolesAsync(u),
isActive = !u.LockoutEnabled || u.LockoutEnd == null || u.LockoutEnd < DateTimeOffset.UtcNow,
createdAt = u.CreatedTime,
});
return Ok(await Task.WhenAll(result));
}
[HttpGet("{tenantId}/roles")]
public async Task<ActionResult<List<object>>> GetTenantRoles(string tenantId)
{
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.TenantId == tenantId);
if (tenant == null)
{
return NotFound();
}
var roles = await _context.Roles
.Where(r => r.TenantId == tenant.Id)
.ToListAsync();
var result = roles.Select(r => new
{
id = r.Id,
name = r.Name,
displayName = r.DisplayName,
});
return Ok(result);
}
[HttpGet("{tenantId}/settings")]
public async Task<ActionResult<TenantSettings>> GetTenantSettings(string tenantId)
{
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.TenantId == tenantId);
if (tenant == null)
{
return NotFound();
}
var settings = new TenantSettings
{
AllowRegistration = false,
AllowedEmailDomains = "",
DefaultRoleId = null,
PasswordPolicy = new List<string> { "requireNumber", "requireLowercase" },
MinPasswordLength = 8,
SessionTimeout = 120,
};
return Ok(settings);
}
[HttpPut("{tenantId}/settings")]
public async Task<IActionResult> UpdateTenantSettings(string tenantId, TenantSettings settings)
{
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.TenantId == tenantId);
if (tenant == null)
{
return NotFound();
}
await CreateAuditLog("tenant", "update", "TenantSettings", tenant.Id, tenant.TenantId, null, JsonSerializer.Serialize(settings));
return NoContent();
}
[HttpPost]
public async Task<ActionResult<Tenant>> CreateTenant(CreateTenantDto dto)
{
var tenant = new Tenant
{
TenantId = dto.TenantId,
Name = dto.Name,
ContactName = dto.ContactName,
ContactEmail = dto.ContactEmail,
ContactPhone = dto.ContactPhone,
MaxUsers = dto.MaxUsers,
Description = dto.Description,
Status = dto.Status,
ExpiresAt = dto.ExpiresAt,
CreatedAt = DateTime.UtcNow,
};
_context.Tenants.Add(tenant);
await _context.SaveChangesAsync();
await CreateAuditLog("tenant", "create", "Tenant", tenant.Id, tenant.TenantId, null, JsonSerializer.Serialize(dto));
return CreatedAtAction(nameof(GetTenant), new { id = tenant.Id }, tenant);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateTenant(long id, UpdateTenantDto dto)
{
var tenant = await _context.Tenants.FindAsync(id);
if (tenant == null)
{
return NotFound();
}
var oldValue = JsonSerializer.Serialize(tenant);
tenant.Name = dto.Name;
tenant.ContactName = dto.ContactName;
tenant.ContactEmail = dto.ContactEmail;
tenant.ContactPhone = dto.ContactPhone;
tenant.MaxUsers = dto.MaxUsers;
tenant.Description = dto.Description;
tenant.Status = dto.Status;
tenant.ExpiresAt = dto.ExpiresAt;
tenant.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await CreateAuditLog("tenant", "update", "Tenant", tenant.Id, tenant.TenantId, oldValue, JsonSerializer.Serialize(tenant));
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTenant(long id)
{
var tenant = await _context.Tenants.FindAsync(id);
if (tenant == null)
{
return NotFound();
}
var oldValue = JsonSerializer.Serialize(tenant);
var users = await _context.Users.Where(u => u.TenantId == tenant.Id).ToListAsync();
foreach (var user in users)
{
user.IsDeleted = true;
user.UpdatedTime = DateTime.UtcNow;
}
tenant.IsDeleted = true;
await _context.SaveChangesAsync();
await CreateAuditLog("tenant", "delete", "Tenant", tenant.Id, tenant.TenantId, oldValue);
return NoContent();
}
private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null)
{
var userName = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "system";
var tenantId = User.FindFirstValue("TenantId");
var log = new AuditLog
{
Operator = userName,
TenantId = tenantId,
Operation = operation,
Action = action,
TargetType = targetType,
TargetId = targetId,
TargetName = targetName,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
Status = "success",
OldValue = oldValue,
NewValue = newValue,
};
_context.AuditLogs.Add(log);
await _context.SaveChangesAsync();
}
}
public class CreateTenantDto
{
public string TenantId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string ContactName { get; set; } = string.Empty;
public string ContactEmail { get; set; } = string.Empty;
public string? ContactPhone { get; set; }
public int? MaxUsers { get; set; }
public string? Description { get; set; }
public string Status { get; set; } = "active";
public DateTime? ExpiresAt { get; set; }
}
public class UpdateTenantDto
{
public string Name { get; set; } = string.Empty;
public string ContactName { get; set; } = string.Empty;
public string ContactEmail { get; set; } = string.Empty;
public string? ContactPhone { get; set; }
public int? MaxUsers { get; set; }
public string? Description { get; set; }
public string Status { get; set; } = "active";
public DateTime? ExpiresAt { get; set; }
}
public class TenantSettings
{
public bool AllowRegistration { get; set; }
public string AllowedEmailDomains { get; set; } = string.Empty;
public long? DefaultRoleId { get; set; }
public List<string> PasswordPolicy { get; set; } = new();
public int MinPasswordLength { get; set; } = 8;
public int SessionTimeout { get; set; } = 120;
}

View File

@ -1,255 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("connect")]
public class TokenController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<TokenController> logger)
: ControllerBase
{
private readonly ILogger<TokenController> _logger = logger;
[HttpPost("token")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("OpenIddict request is null");
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (request.IsAuthorizationCodeGrantType())
{
return await ExchangeAuthorizationCodeAsync(request, result);
}
if (request.IsRefreshTokenGrantType())
{
return await ExchangeRefreshTokenAsync(request);
}
if (request.IsPasswordGrantType())
{
return await ExchangePasswordAsync(request);
}
return BadRequest(new OpenIddictResponse
{
Error = Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
private async Task<IActionResult> ExchangeAuthorizationCodeAsync(OpenIddictRequest request,
AuthenticateResult result)
{
var application = await applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
return BadRequest(new OpenIddictResponse
{
Error = Errors.InvalidClient,
ErrorDescription = "The specified client is invalid."
});
}
var authorization = await authorizationManager.FindAsync(
subject: result.Principal?.GetClaim(Claims.Subject),
client: await applicationManager.GetIdAsync(application),
status: Statuses.Valid,
type: AuthorizationTypes.Permanent,
scopes: request.GetScopes()).FirstOrDefaultAsync();
if (authorization == null)
{
return BadRequest(new OpenIddictResponse
{
Error = Errors.InvalidGrant,
ErrorDescription = "The authorization code is invalid."
});
}
var user = await userManager.FindByIdAsync(result.Principal?.GetClaim(Claims.Subject));
if (user == null || user.IsDeleted)
{
return BadRequest(new OpenIddictResponse
{
Error = Errors.InvalidGrant,
ErrorDescription = "The user is no longer valid."
});
}
var claims = new List<Claim>
{
new(Claims.Subject, await userManager.GetUserIdAsync(user)),
new(Claims.Name, await userManager.GetUserNameAsync(user)),
new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""),
new("tenant_id", user.TenantId.ToString())
};
var roles = await userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(Claims.Role, role));
}
var identity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
principal.SetScopes(request.GetScopes());
principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
private async Task<IActionResult> ExchangeRefreshTokenAsync(OpenIddictRequest request)
{
var principalResult =
await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
if (principalResult is not { Succeeded: true })
{
return BadRequest(new OpenIddictResponse
{
Error = Errors.InvalidGrant,
ErrorDescription = "The refresh token is invalid."
});
}
var user = await userManager.GetUserAsync(principalResult.Principal);
if (user == null || user.IsDeleted)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}!));
}
// Ensure the user is still allowed to sign in.
if (!await signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The user is no longer allowed to sign in."
}!));
}
var principal = principalResult.Principal;
foreach (var claim in principal!.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
principal.SetScopes(request.GetScopes());
principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case OpenIddictConstants.Claims.Name:
yield return OpenIddictConstants.Destinations.AccessToken;
if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Profile))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
case OpenIddictConstants.Claims.Email:
yield return OpenIddictConstants.Destinations.AccessToken;
if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Email))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
case OpenIddictConstants.Claims.Role:
yield return OpenIddictConstants.Destinations.AccessToken;
if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Roles))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return OpenIddictConstants.Destinations.AccessToken;
yield break;
}
}
private async Task<IActionResult> ExchangePasswordAsync(OpenIddictRequest request)
{
var user = await userManager.FindByNameAsync(request.Username);
if (user == null || user.IsDeleted)
{
return BadRequest(new OpenIddictResponse
{
Error = Errors.InvalidGrant,
ErrorDescription = "用户名或密码错误"
});
}
var signInResult = await signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (!signInResult.Succeeded)
{
return BadRequest(new OpenIddictResponse
{
Error = Errors.InvalidGrant,
ErrorDescription = "用户名或密码错误"
});
}
var claims = new List<Claim>
{
new(Claims.Subject, await userManager.GetUserIdAsync(user)),
new(Claims.Name, await userManager.GetUserNameAsync(user)),
new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""),
new("tenant_id", user.TenantId.ToString())
};
var roles = await userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(Claims.Role, role));
}
var identity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
principal.SetScopes(request.GetScopes());
principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}

View File

@ -1,291 +0,0 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class UsersController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<ApplicationRole> _roleManager;
private readonly ILogger<UsersController> _logger;
public UsersController(
ApplicationDbContext context,
UserManager<ApplicationUser> userManager,
RoleManager<ApplicationRole> roleManager,
ILogger<UsersController> logger)
{
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<object>> GetUsers(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? userName = null,
[FromQuery] string? email = null,
[FromQuery] string? tenantId = null)
{
var query = _context.Users.AsQueryable();
if (!string.IsNullOrEmpty(userName))
{
query = query.Where(u => u.UserName!.Contains(userName));
}
if (!string.IsNullOrEmpty(email))
{
query = query.Where(u => u.Email != null && u.Email.Contains(email));
}
if (!string.IsNullOrEmpty(tenantId))
{
query = query.Where(u => u.TenantId.ToString() == tenantId);
}
var totalCount = await query.CountAsync();
var users = await query
.OrderByDescending(u => u.CreatedTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var result = users.Select(async u => new
{
id = u.Id,
userName = u.UserName,
email = u.Email,
realName = u.RealName,
phone = u.Phone,
tenantId = u.TenantId,
roles = (await _userManager.GetRolesAsync(u)).ToList(),
emailConfirmed = u.EmailConfirmed,
isActive = !u.LockoutEnabled || u.LockoutEnd == null || u.LockoutEnd < DateTimeOffset.UtcNow,
createdAt = u.CreatedTime,
});
return Ok(new
{
items = await Task.WhenAll(result),
totalCount,
page,
pageSize
});
}
[HttpGet("{id}")]
public async Task<ActionResult<object>> GetUser(long id)
{
var user = await _context.Users.FindAsync(id);
if (user == null)
{
return NotFound();
}
var roles = await _userManager.GetRolesAsync(user);
return Ok(new
{
id = user.Id,
userName = user.UserName,
email = user.Email,
realName = user.RealName,
phone = user.Phone,
tenantId = user.TenantId,
roles,
emailConfirmed = user.EmailConfirmed,
isActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow,
createdAt = user.CreatedTime,
});
}
[HttpPost]
public async Task<ActionResult<ApplicationUser>> CreateUser(CreateUserDto dto)
{
var user = new ApplicationUser
{
UserName = dto.UserName,
Email = dto.Email,
RealName = dto.RealName,
Phone = dto.Phone,
TenantId = dto.TenantId ?? 0,
EmailConfirmed = dto.EmailConfirmed,
CreatedTime = DateTime.UtcNow,
};
var result = await _userManager.CreateAsync(user, dto.Password);
if (!result.Succeeded)
{
return BadRequest(result.Errors);
}
if (dto.RoleIds != null && dto.RoleIds.Any())
{
foreach (var roleId in dto.RoleIds)
{
var role = await _roleManager.FindByIdAsync(roleId.ToString());
if (role != null)
{
await _userManager.AddToRoleAsync(user, role.Name!);
}
}
}
if (!dto.IsActive)
{
await _userManager.SetLockoutEnabledAsync(user, true);
await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue);
}
await CreateAuditLog("user", "create", "User", user.Id, user.UserName, null, SerializeToJson(dto));
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUser(long id, UpdateUserDto dto)
{
var user = await _context.Users.FindAsync(id);
if (user == null)
{
return NotFound();
}
var oldValue = System.Text.Json.JsonSerializer.Serialize(user);
user.Email = dto.Email;
user.RealName = dto.RealName;
user.Phone = dto.Phone;
user.EmailConfirmed = dto.EmailConfirmed;
user.UpdatedTime = DateTime.UtcNow;
if (dto.IsActive)
{
await _userManager.SetLockoutEnabledAsync(user, false);
await _userManager.SetLockoutEndDateAsync(user, null);
}
else
{
await _userManager.SetLockoutEnabledAsync(user, true);
await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.MaxValue);
}
await _context.SaveChangesAsync();
await CreateAuditLog("user", "update", "User", user.Id, user.UserName, oldValue, System.Text.Json.JsonSerializer.Serialize(user));
return NoContent();
}
[HttpPut("{id}/password")]
public async Task<IActionResult> ResetPassword(long id, ResetPasswordDto dto)
{
var user = await _userManager.FindByIdAsync(id.ToString());
if (user == null)
{
return NotFound();
}
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var result = await _userManager.ResetPasswordAsync(user, token, dto.NewPassword);
if (!result.Succeeded)
{
return BadRequest(result.Errors);
}
await CreateAuditLog("user", "reset_password", "User", user.Id, user.UserName);
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUser(long id)
{
var user = await _context.Users.FindAsync(id);
if (user == null)
{
return NotFound();
}
var oldValue = System.Text.Json.JsonSerializer.Serialize(user);
user.IsDeleted = true;
user.UpdatedTime = DateTime.UtcNow;
await _context.SaveChangesAsync();
await CreateAuditLog("user", "delete", "User", user.Id, user.UserName, oldValue);
return NoContent();
}
private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null)
{
var userName = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "system";
var tenantId = User.FindFirstValue("TenantId");
var log = new AuditLog
{
Operator = userName,
TenantId = tenantId,
Operation = operation,
Action = action,
TargetType = targetType,
TargetId = targetId,
TargetName = targetName,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
Status = "success",
OldValue = oldValue,
NewValue = newValue,
};
_context.AuditLogs.Add(log);
await _context.SaveChangesAsync();
}
private string SerializeToJson(object obj)
{
return System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = false
});
}
}
public class CreateUserDto
{
public string UserName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string RealName { get; set; } = string.Empty;
public string? Phone { get; set; }
public long? TenantId { get; set; }
public List<long> RoleIds { get; set; } = new();
public string Password { get; set; } = string.Empty;
public bool EmailConfirmed { get; set; }
public bool IsActive { get; set; } = true;
}
public class UpdateUserDto
{
public string Email { get; set; } = string.Empty;
public string RealName { get; set; } = string.Empty;
public string? Phone { get; set; }
public bool EmailConfirmed { get; set; }
public bool IsActive { get; set; } = true;
}
public class ResetPasswordDto
{
public string NewPassword { get; set; } = string.Empty;
}

View File

@ -1,99 +0,0 @@
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Data;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, long>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<OAuthApplication> OAuthApplications { get; set; }
public DbSet<Tenant> Tenants { get; set; }
public DbSet<AccessLog> AccessLogs { get; set; }
public DbSet<AuditLog> AuditLogs { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<ApplicationUser>(entity =>
{
entity.Property(e => e.RealName).HasMaxLength(100);
entity.Property(e => e.Phone).HasMaxLength(20);
entity.HasIndex(e => e.TenantId);
entity.HasIndex(e => e.Phone).IsUnique();
});
builder.Entity<ApplicationRole>(entity =>
{
entity.Property(e => e.Description).HasMaxLength(200);
});
builder.Entity<OAuthApplication>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.ClientId).IsUnique();
entity.Property(e => e.ClientId).HasMaxLength(100);
entity.Property(e => e.ClientSecret).HasMaxLength(200);
entity.Property(e => e.DisplayName).HasMaxLength(100);
entity.Property(e => e.ClientType).HasMaxLength(20);
entity.Property(e => e.ConsentType).HasMaxLength(20);
entity.Property(e => e.Status).HasMaxLength(20);
entity.Property(e => e.Description).HasMaxLength(500);
});
builder.Entity<Tenant>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.TenantId).IsUnique();
entity.Property(e => e.TenantId).HasMaxLength(50);
entity.Property(e => e.Name).HasMaxLength(100);
entity.Property(e => e.ContactName).HasMaxLength(50);
entity.Property(e => e.ContactEmail).HasMaxLength(100);
entity.Property(e => e.ContactPhone).HasMaxLength(20);
entity.Property(e => e.Status).HasMaxLength(20);
entity.Property(e => e.Description).HasMaxLength(500);
});
builder.Entity<AccessLog>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.CreatedAt);
entity.HasIndex(e => e.UserName);
entity.HasIndex(e => e.TenantId);
entity.HasIndex(e => e.Action);
entity.HasIndex(e => e.Status);
entity.Property(e => e.UserName).HasMaxLength(50);
entity.Property(e => e.TenantId).HasMaxLength(50);
entity.Property(e => e.Action).HasMaxLength(20);
entity.Property(e => e.Resource).HasMaxLength(200);
entity.Property(e => e.Method).HasMaxLength(10);
entity.Property(e => e.IpAddress).HasMaxLength(50);
entity.Property(e => e.UserAgent).HasMaxLength(500);
entity.Property(e => e.Status).HasMaxLength(20);
});
builder.Entity<AuditLog>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.CreatedAt);
entity.HasIndex(e => e.Operator);
entity.HasIndex(e => e.TenantId);
entity.HasIndex(e => e.Operation);
entity.HasIndex(e => e.Action);
entity.Property(e => e.Operator).HasMaxLength(50);
entity.Property(e => e.TenantId).HasMaxLength(50);
entity.Property(e => e.Operation).HasMaxLength(20);
entity.Property(e => e.Action).HasMaxLength(20);
entity.Property(e => e.TargetType).HasMaxLength(50);
entity.Property(e => e.TargetName).HasMaxLength(100);
entity.Property(e => e.IpAddress).HasMaxLength(50);
entity.Property(e => e.Description).HasMaxLength(500);
entity.Property(e => e.Status).HasMaxLength(20);
});
}
}

View File

@ -1,16 +0,0 @@
using Fengling.AuthService.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Fengling.AuthService.Data;
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseNpgsql("Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542");
return new ApplicationDbContext(optionsBuilder.Options);
}
}

View File

@ -1,312 +0,0 @@
// <auto-generated />
using System;
using Fengling.AuthService.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.AuthService.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260201153600_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationRole", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("Phone")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("RealName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<long>("TenantId")
.HasColumnType("bigint");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("Phone")
.IsUnique();
b.HasIndex("TenantId");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,244 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.AuthService.Data.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Description = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RealName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
TenantId = table.Column<long>(type: "bigint", nullable: false),
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<long>(type: "bigint", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<long>(type: "bigint", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "text", nullable: false),
ProviderKey = table.Column<string>(type: "text", nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<long>(type: "bigint", nullable: false),
RoleId = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<long>(type: "bigint", nullable: false),
LoginProvider = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "IX_AspNetUsers_Phone",
table: "AspNetUsers",
column: "Phone",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUsers_TenantId",
table: "AspNetUsers",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}

View File

@ -1,379 +0,0 @@
// <auto-generated />
using System;
using Fengling.AuthService.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.AuthService.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260202015716_AddOAuthApplications")]
partial class AddOAuthApplications
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationRole", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("Phone")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("RealName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<long>("TenantId")
.HasColumnType("bigint");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("Phone")
.IsUnique();
b.HasIndex("TenantId");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.OAuthApplication", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ClientSecret")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("ConsentType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.PrimitiveCollection<string[]>("GrantTypes")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("PostLogoutRedirectUris")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("RedirectUris")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("Scopes")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OAuthApplications");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,53 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.AuthService.Data.Migrations
{
/// <inheritdoc />
public partial class AddOAuthApplications : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OAuthApplications",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ClientId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
ClientSecret = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
DisplayName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
RedirectUris = table.Column<string[]>(type: "text[]", nullable: false),
PostLogoutRedirectUris = table.Column<string[]>(type: "text[]", nullable: false),
Scopes = table.Column<string[]>(type: "text[]", nullable: false),
GrantTypes = table.Column<string[]>(type: "text[]", nullable: false),
ClientType = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
ConsentType = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OAuthApplications", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_OAuthApplications_ClientId",
table: "OAuthApplications",
column: "ClientId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OAuthApplications");
}
}
}

View File

@ -1,621 +0,0 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Fengling.AuthService.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.AuthService.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260202031310_AddTenantAndLogs")]
partial class AddTenantAndLogs
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Fengling.AuthService.Models.AccessLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Duration")
.HasColumnType("integer");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<string>("IpAddress")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Method")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("RequestData")
.HasColumnType("text");
b.Property<string>("Resource")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ResponseData")
.HasColumnType("text");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TenantId")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("UserAgent")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("UserName")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("Action");
b.HasIndex("CreatedAt");
b.HasIndex("Status");
b.HasIndex("TenantId");
b.HasIndex("UserName");
b.ToTable("AccessLogs");
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationRole", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<bool>("IsSystem")
.HasColumnType("boolean");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.PrimitiveCollection<List<string>>("Permissions")
.HasColumnType("text[]");
b.Property<long?>("TenantId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("Phone")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("RealName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<long>("TenantId")
.HasColumnType("bigint");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("Phone")
.IsUnique();
b.HasIndex("TenantId");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.AuditLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("NewValue")
.HasColumnType("text");
b.Property<string>("OldValue")
.HasColumnType("text");
b.Property<string>("Operation")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Operator")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<string>("TargetName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("TargetType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("TenantId")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("Action");
b.HasIndex("CreatedAt");
b.HasIndex("Operation");
b.HasIndex("Operator");
b.HasIndex("TenantId");
b.ToTable("AuditLogs");
});
modelBuilder.Entity("Fengling.AuthService.Models.OAuthApplication", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ClientSecret")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("ConsentType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.PrimitiveCollection<string[]>("GrantTypes")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("PostLogoutRedirectUris")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("RedirectUris")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("Scopes")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OAuthApplications");
});
modelBuilder.Entity("Fengling.AuthService.Models.Tenant", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ContactEmail")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ContactName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ContactPhone")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int?>("MaxUsers")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TenantId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("TenantId")
.IsUnique();
b.ToTable("Tenants");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{
b.HasOne("Fengling.AuthService.Models.Tenant", null)
.WithMany("Users")
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.AuthService.Models.Tenant", b =>
{
b.Navigation("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,214 +0,0 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.AuthService.Data.Migrations
{
/// <inheritdoc />
public partial class AddTenantAndLogs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DisplayName",
table: "AspNetRoles",
type: "text",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsSystem",
table: "AspNetRoles",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<List<string>>(
name: "Permissions",
table: "AspNetRoles",
type: "text[]",
nullable: true);
migrationBuilder.AddColumn<long>(
name: "TenantId",
table: "AspNetRoles",
type: "bigint",
nullable: true);
migrationBuilder.CreateTable(
name: "AccessLogs",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserName = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
TenantId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
Action = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Resource = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Method = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: true),
IpAddress = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
UserAgent = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Duration = table.Column<int>(type: "integer", nullable: false),
RequestData = table.Column<string>(type: "text", nullable: true),
ResponseData = table.Column<string>(type: "text", nullable: true),
ErrorMessage = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AccessLogs", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AuditLogs",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Operator = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
TenantId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
Operation = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Action = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
TargetType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
TargetId = table.Column<long>(type: "bigint", nullable: true),
TargetName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
IpAddress = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
OldValue = table.Column<string>(type: "text", nullable: true),
NewValue = table.Column<string>(type: "text", nullable: true),
ErrorMessage = table.Column<string>(type: "text", nullable: true),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AuditLogs", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Tenants",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
TenantId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
ContactName = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
ContactEmail = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
ContactPhone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
MaxUsers = table.Column<int>(type: "integer", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tenants", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_AccessLogs_Action",
table: "AccessLogs",
column: "Action");
migrationBuilder.CreateIndex(
name: "IX_AccessLogs_CreatedAt",
table: "AccessLogs",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_AccessLogs_Status",
table: "AccessLogs",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_AccessLogs_TenantId",
table: "AccessLogs",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_AccessLogs_UserName",
table: "AccessLogs",
column: "UserName");
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_Action",
table: "AuditLogs",
column: "Action");
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_CreatedAt",
table: "AuditLogs",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_Operation",
table: "AuditLogs",
column: "Operation");
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_Operator",
table: "AuditLogs",
column: "Operator");
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_TenantId",
table: "AuditLogs",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_Tenants_TenantId",
table: "Tenants",
column: "TenantId",
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_AspNetUsers_Tenants_TenantId",
table: "AspNetUsers",
column: "TenantId",
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AspNetUsers_Tenants_TenantId",
table: "AspNetUsers");
migrationBuilder.DropTable(
name: "AccessLogs");
migrationBuilder.DropTable(
name: "AuditLogs");
migrationBuilder.DropTable(
name: "Tenants");
migrationBuilder.DropColumn(
name: "DisplayName",
table: "AspNetRoles");
migrationBuilder.DropColumn(
name: "IsSystem",
table: "AspNetRoles");
migrationBuilder.DropColumn(
name: "Permissions",
table: "AspNetRoles");
migrationBuilder.DropColumn(
name: "TenantId",
table: "AspNetRoles");
}
}
}

View File

@ -1,625 +0,0 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Fengling.AuthService.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.AuthService.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260202064650_AddOAuthDescription")]
partial class AddOAuthDescription
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Fengling.AuthService.Models.AccessLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Duration")
.HasColumnType("integer");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<string>("IpAddress")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Method")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("RequestData")
.HasColumnType("text");
b.Property<string>("Resource")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ResponseData")
.HasColumnType("text");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TenantId")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("UserAgent")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("UserName")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("Action");
b.HasIndex("CreatedAt");
b.HasIndex("Status");
b.HasIndex("TenantId");
b.HasIndex("UserName");
b.ToTable("AccessLogs");
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationRole", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<bool>("IsSystem")
.HasColumnType("boolean");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.PrimitiveCollection<List<string>>("Permissions")
.HasColumnType("text[]");
b.Property<long?>("TenantId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("Phone")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("RealName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<long>("TenantId")
.HasColumnType("bigint");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("Phone")
.IsUnique();
b.HasIndex("TenantId");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.AuditLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("NewValue")
.HasColumnType("text");
b.Property<string>("OldValue")
.HasColumnType("text");
b.Property<string>("Operation")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Operator")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<string>("TargetName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("TargetType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("TenantId")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("Action");
b.HasIndex("CreatedAt");
b.HasIndex("Operation");
b.HasIndex("Operator");
b.HasIndex("TenantId");
b.ToTable("AuditLogs");
});
modelBuilder.Entity("Fengling.AuthService.Models.OAuthApplication", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ClientSecret")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("ConsentType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.PrimitiveCollection<string[]>("GrantTypes")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("PostLogoutRedirectUris")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("RedirectUris")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("Scopes")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OAuthApplications");
});
modelBuilder.Entity("Fengling.AuthService.Models.Tenant", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ContactEmail")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ContactName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ContactPhone")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int?>("MaxUsers")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TenantId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("TenantId")
.IsUnique();
b.ToTable("Tenants");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{
b.HasOne("Fengling.AuthService.Models.Tenant", null)
.WithMany("Users")
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.AuthService.Models.Tenant", b =>
{
b.Navigation("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Fengling.AuthService.Data.Migrations
{
/// <inheritdoc />
public partial class AddOAuthDescription : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Description",
table: "OAuthApplications",
type: "character varying(500)",
maxLength: 500,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Description",
table: "OAuthApplications");
}
}
}

View File

@ -1,622 +0,0 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Fengling.AuthService.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.AuthService.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Fengling.AuthService.Models.AccessLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Duration")
.HasColumnType("integer");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<string>("IpAddress")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Method")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("RequestData")
.HasColumnType("text");
b.Property<string>("Resource")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ResponseData")
.HasColumnType("text");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TenantId")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("UserAgent")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("UserName")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("Action");
b.HasIndex("CreatedAt");
b.HasIndex("Status");
b.HasIndex("TenantId");
b.HasIndex("UserName");
b.ToTable("AccessLogs");
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationRole", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<bool>("IsSystem")
.HasColumnType("boolean");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.PrimitiveCollection<List<string>>("Permissions")
.HasColumnType("text[]");
b.Property<long?>("TenantId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("Phone")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("RealName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<long>("TenantId")
.HasColumnType("bigint");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("Phone")
.IsUnique();
b.HasIndex("TenantId");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.AuditLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("NewValue")
.HasColumnType("text");
b.Property<string>("OldValue")
.HasColumnType("text");
b.Property<string>("Operation")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Operator")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<string>("TargetName")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("TargetType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("TenantId")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("Action");
b.HasIndex("CreatedAt");
b.HasIndex("Operation");
b.HasIndex("Operator");
b.HasIndex("TenantId");
b.ToTable("AuditLogs");
});
modelBuilder.Entity("Fengling.AuthService.Models.OAuthApplication", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ClientSecret")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClientType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("ConsentType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.PrimitiveCollection<string[]>("GrantTypes")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("PostLogoutRedirectUris")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("RedirectUris")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("Scopes")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OAuthApplications");
});
modelBuilder.Entity("Fengling.AuthService.Models.Tenant", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ContactEmail")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ContactName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ContactPhone")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int?>("MaxUsers")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("TenantId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("TenantId")
.IsUnique();
b.ToTable("Tenants");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{
b.HasOne("Fengling.AuthService.Models.Tenant", null)
.WithMany("Users")
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<long>", b =>
{
b.HasOne("Fengling.AuthService.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.AuthService.Models.Tenant", b =>
{
b.Navigation("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,143 +0,0 @@
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Data;
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider)
{
using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
context.Database.EnsureCreated();
var defaultTenant = await context.Tenants
.FirstOrDefaultAsync(t => t.TenantId == "default");
if (defaultTenant == null)
{
defaultTenant = new Tenant
{
TenantId = "default",
Name = "默认租户",
ContactName = "系统管理员",
ContactEmail = "admin@fengling.local",
ContactPhone = "13800138000",
MaxUsers = 1000,
Description = "系统默认租户",
Status = "active",
CreatedAt = DateTime.UtcNow
};
context.Tenants.Add(defaultTenant);
await context.SaveChangesAsync();
}
var adminRole = await roleManager.FindByNameAsync("Admin");
if (adminRole == null)
{
adminRole = new ApplicationRole
{
Name = "Admin",
DisplayName = "管理员",
Description = "System administrator",
TenantId = defaultTenant.Id,
IsSystem = true,
Permissions = new List<string>
{
"user.manage", "user.view",
"role.manage", "role.view",
"tenant.manage", "tenant.view",
"oauth.manage", "oauth.view",
"log.view", "system.config"
},
CreatedTime = DateTime.UtcNow
};
await roleManager.CreateAsync(adminRole);
}
var userRole = await roleManager.FindByNameAsync("User");
if (userRole == null)
{
userRole = new ApplicationRole
{
Name = "User",
DisplayName = "普通用户",
Description = "Regular user",
TenantId = defaultTenant.Id,
IsSystem = true,
Permissions = new List<string> { "user.view" },
CreatedTime = DateTime.UtcNow
};
await roleManager.CreateAsync(userRole);
}
var adminUser = await userManager.FindByNameAsync("admin");
if (adminUser == null)
{
adminUser = new ApplicationUser
{
UserName = "admin",
Email = "admin@fengling.local",
RealName = "系统管理员",
Phone = "13800138000",
TenantId = defaultTenant.Id,
EmailConfirmed = true,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
};
var result = await userManager.CreateAsync(adminUser, "Admin@123");
if (result.Succeeded)
{
await userManager.AddToRoleAsync(adminUser, "Admin");
}
}
var testUser = await userManager.FindByNameAsync("testuser");
if (testUser == null)
{
testUser = new ApplicationUser
{
UserName = "testuser",
Email = "test@fengling.local",
RealName = "测试用户",
Phone = "13900139000",
TenantId = defaultTenant.Id,
EmailConfirmed = true,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
};
var result = await userManager.CreateAsync(testUser, "Test@123");
if (result.Succeeded)
{
await userManager.AddToRoleAsync(testUser, "User");
}
}
var consoleClient = await context.OAuthApplications
.FirstOrDefaultAsync(c => c.ClientId == "fengling-console");
if (consoleClient == null)
{
consoleClient = new OAuthApplication
{
ClientId = "fengling-console",
ClientSecret = "console-secret-change-in-production",
DisplayName = "Fengling 运管中心",
RedirectUris = new[] { "http://console.fengling.local/auth/callback" },
PostLogoutRedirectUris = new[] { "http://console.fengling.local/" },
Scopes = new[] { "api", "offline_access" },
GrantTypes = new[] { "authorization_code", "refresh_token" },
ClientType = "confidential",
ConsentType = "implicit",
Status = "active",
CreatedAt = DateTime.UtcNow
};
context.OAuthApplications.Add(consoleClient);
await context.SaveChangesAsync();
}
}
}

View File

@ -1,19 +0,0 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["Fengling.AuthService.csproj", "./"]
RUN dotnet restore "Fengling.AuthService.csproj"
COPY . .
WORKDIR "/src"
RUN dotnet build "Fengling.AuthService.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Fengling.AuthService.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Fengling.AuthService.dll"]

View File

@ -1,35 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Npgsql" Version="9.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="7.2.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="7.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="OpenTelemetry" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Testing.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -1,6 +0,0 @@
@Fengling.AuthService_HostAddress = http://localhost:5132
GET {{Fengling.AuthService_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -1,43 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Fengling.AuthService.Models;
public class AccessLog
{
[Key]
public long Id { get; set; }
[MaxLength(50)]
public string? UserName { get; set; }
[MaxLength(50)]
public string? TenantId { get; set; }
[MaxLength(20)]
public string Action { get; set; } = string.Empty;
[MaxLength(200)]
public string? Resource { get; set; }
[MaxLength(10)]
public string? Method { get; set; }
[MaxLength(50)]
public string? IpAddress { get; set; }
[MaxLength(500)]
public string? UserAgent { get; set; }
[MaxLength(20)]
public string Status { get; set; } = "success";
public int Duration { get; set; }
public string? RequestData { get; set; }
public string? ResponseData { get; set; }
public string? ErrorMessage { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@ -1,14 +0,0 @@
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations.Schema;
namespace Fengling.AuthService.Models;
public class ApplicationRole : IdentityRole<long>
{
public string? Description { get; set; }
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
public long? TenantId { get; set; }
public bool IsSystem { get; set; }
public string? DisplayName { get; set; }
public List<string>? Permissions { get; set; }
}

View File

@ -1,13 +0,0 @@
using Microsoft.AspNetCore.Identity;
namespace Fengling.AuthService.Models;
public class ApplicationUser : IdentityUser<long>
{
public string? RealName { get; set; }
public string? Phone { get; set; }
public long TenantId { get; set; }
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedTime { get; set; }
public bool IsDeleted { get; set; }
}

View File

@ -1,47 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Fengling.AuthService.Models;
public class AuditLog
{
[Key]
public long Id { get; set; }
[MaxLength(50)]
[Required]
public string Operator { get; set; } = string.Empty;
[MaxLength(50)]
public string? TenantId { get; set; }
[MaxLength(20)]
public string Operation { get; set; } = string.Empty;
[MaxLength(20)]
public string Action { get; set; } = string.Empty;
[MaxLength(50)]
public string? TargetType { get; set; }
public long? TargetId { get; set; }
[MaxLength(100)]
public string? TargetName { get; set; }
[MaxLength(50)]
public string IpAddress { get; set; } = string.Empty;
[MaxLength(500)]
public string? Description { get; set; }
public string? OldValue { get; set; }
public string? NewValue { get; set; }
public string? ErrorMessage { get; set; }
[MaxLength(20)]
public string Status { get; set; } = "success";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@ -1,21 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Fengling.AuthService.Models;
public class OAuthApplication
{
public long Id { get; set; }
public string ClientId { get; set; } = string.Empty;
public string? ClientSecret { get; set; }
public string DisplayName { get; set; } = string.Empty;
public string[] RedirectUris { get; set; } = Array.Empty<string>();
public string[] PostLogoutRedirectUris { get; set; } = Array.Empty<string>();
public string[] Scopes { get; set; } = Array.Empty<string>();
public string[] GrantTypes { get; set; } = Array.Empty<string>();
public string ClientType { get; set; } = "public";
public string ConsentType { get; set; } = "implicit";
public string Status { get; set; } = "active";
public string? Description { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}

View File

@ -1,47 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Fengling.AuthService.Models;
public class Tenant
{
[Key]
public long Id { get; set; }
[MaxLength(50)]
[Required]
public string TenantId { get; set; } = string.Empty;
[MaxLength(100)]
[Required]
public string Name { get; set; } = string.Empty;
[MaxLength(50)]
[Required]
public string ContactName { get; set; } = string.Empty;
[MaxLength(100)]
[Required]
[EmailAddress]
public string ContactEmail { get; set; } = string.Empty;
[MaxLength(20)]
public string? ContactPhone { get; set; }
public int? MaxUsers { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[MaxLength(500)]
public string? Description { get; set; }
[MaxLength(20)]
public string Status { get; set; } = "active";
public DateTime? ExpiresAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
public ICollection<ApplicationUser> Users { get; set; } = new List<ApplicationUser>();
}

View File

@ -1,120 +0,0 @@
using Fengling.AuthService.Configuration;
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
builder.Host.UseSerilog();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
if (connectionString.StartsWith("DataSource="))
{
options.UseInMemoryDatabase(connectionString);
}
else
{
options.UseNpgsql(connectionString);
}
});
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "Fengling.Auth";
options.Cookie.SecurePolicy = CookieSecurePolicy.None;
options.Cookie.SameSite = SameSiteMode.Lax;
options.ExpireTimeSpan = TimeSpan.FromDays(7);
});
builder.Services.AddOpenIddictConfiguration(builder.Configuration);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService("Fengling.AuthService"))
.WithTracing(tracing =>
tracing.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource("OpenIddict.Server.AspNetCore")
.AddOtlpExporter());
builder.Services.AddControllersWithViews();
builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("DefaultConnection")!);
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Fengling Auth Service",
Version = "v1",
Description = "Authentication and authorization service using OpenIddict"
});
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Password = new OpenApiOAuthFlow
{
TokenUrl = new Uri("/connect/token", UriKind.Relative)
}
}
});
});
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
await SeedData.Initialize(scope.ServiceProvider);
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
var isTesting = builder.Configuration.GetValue<bool>("Testing", false);
if (!isTesting)
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Fengling Auth Service v1");
options.OAuthClientId("swagger-ui");
options.OAuthUsePkce();
});
}
app.MapRazorPages();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();

View File

@ -1,14 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5132",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -1,61 +0,0 @@
# Fengling Auth Service
Authentication and authorization service using OpenIddict.
## Features
- JWT token issuance
- OAuth2/OIDC support
- Multi-tenant support (TenantId in JWT claims)
- Role-based access control (RBAC)
- Health check endpoint
## API Endpoints
### Get Token
```
POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password
username={username}
password={password}
scope=api offline_access
```
### Health Check
```
GET /health
```
## Default Users
- **Admin**: username=admin, password=Admin@123, role=Admin
- **Test User**: username=testuser, password=Test@123, role=User
## Running Locally
```bash
dotnet run
```
Service runs on port 5000.
## Docker
```bash
docker build -t fengling-auth:latest .
docker run -p 5000:80 fengling-auth:latest
```
## Environment Variables
- `ConnectionStrings__DefaultConnection`: PostgreSQL connection string
- `OpenIddict__Issuer`: Token issuer URL
- `OpenIddict__Audience`: Token audience
## Database
- PostgreSQL
- Uses ASP.NET Core Identity for user/role management
- Tenant isolation via `TenantId` column

View File

@ -1,6 +0,0 @@
namespace Fengling.AuthService.ViewModels;
public record AuthorizeViewModel(string? ApplicationName, string? Scope)
{
public string[]? Scopes => Scope?.Split(' ') ?? null;
}

View File

@ -1,7 +0,0 @@
namespace Fengling.AuthService.ViewModels;
public class DashboardViewModel
{
public string? Username { get; set; }
public string? Email { get; set; }
}

View File

@ -1,14 +0,0 @@
namespace Fengling.AuthService.ViewModels;
public class LoginViewModel
{
public string ReturnUrl { get; set; }
}
public class LoginInputModel
{
public string Username { get; set; }
public string Password { get; set; }
public bool RememberMe { get; set; }
public string ReturnUrl { get; set; }
}

View File

@ -1,26 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Fengling.AuthService.ViewModels;
public class RegisterViewModel
{
[Required(ErrorMessage = "用户名不能为空")]
[StringLength(50, MinimumLength = 3, ErrorMessage = "用户名长度必须在3-50个字符之间")]
public string Username { get; set; }
[Required(ErrorMessage = "邮箱不能为空")]
[EmailAddress(ErrorMessage = "请输入有效的邮箱地址")]
public string Email { get; set; }
[Required(ErrorMessage = "密码不能为空")]
[StringLength(100, MinimumLength = 6, ErrorMessage = "密码长度必须在6-100个字符之间")]
[DataType(DataType.Password)]
public string Password { get; set; }
[Required(ErrorMessage = "确认密码不能为空")]
[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = "两次输入的密码不一致")]
public string ConfirmPassword { get; set; }
public string ReturnUrl { get; set; }
}

View File

@ -1,81 +0,0 @@
@model Fengling.AuthService.ViewModels.LoginInputModel
@{
Layout = "_Layout";
ViewData["Title"] = "登录";
}
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center px-4 py-8">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<div class="inline-flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 mb-4">
<svg class="h-8 w-8 text-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5"/>
<path d="M2 12l10 5 10-5"/>
</svg>
</div>
<h1 class="text-2xl font-bold">欢迎回来</h1>
<p class="text-muted-foreground mt-2">登录到 Fengling Auth</p>
</div>
<div class="bg-card border border-border rounded-lg shadow-sm p-6">
@if (!ViewData.ModelState.IsValid)
{
<div class="mb-4 p-3 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm">
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
<p>@error.ErrorMessage</p>
}
</div>
}
<form method="post" class="space-y-4">
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" asp-for="ReturnUrl" />
<div class="space-y-2">
<label for="Username" class="text-sm font-medium">用户名</label>
<input type="text"
id="Username"
name="Username"
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="请输入用户名"
value="@Model.Username"
required
autocomplete="username">
</div>
<div class="space-y-2">
<label for="Password" class="text-sm font-medium">密码</label>
<input type="password"
id="Password"
name="Password"
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="请输入密码"
required
autocomplete="current-password">
</div>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<input type="checkbox"
id="RememberMe"
name="RememberMe"
class="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-ring focus:ring-offset-2">
<label for="RememberMe" class="text-sm font-medium">记住我</label>
</div>
</div>
<button type="submit"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-11 w-full px-8 shadow">
登录
</button>
</form>
</div>
<div class="mt-6 text-center text-sm">
<span class="text-muted-foreground">还没有账号?</span>
<a href="/account/register?returnUrl=@Model.ReturnUrl" class="text-primary hover:underline ml-1">立即注册</a>
</div>
</div>
</div>

View File

@ -1,95 +0,0 @@
@model Fengling.AuthService.ViewModels.RegisterViewModel
@{
Layout = "_Layout";
ViewData["Title"] = "注册";
}
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center px-4 py-8">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<div class="inline-flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 mb-4">
<svg class="h-8 w-8 text-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="8.5" cy="7" r="4"/>
<line x1="20" y1="8" x2="20" y2="14"/>
<line x1="23" y1="11" x2="17" y2="11"/>
</svg>
</div>
<h1 class="text-2xl font-bold">创建账号</h1>
<p class="text-muted-foreground mt-2">加入 Fengling Auth</p>
</div>
<div class="bg-card border border-border rounded-lg shadow-sm p-6">
@if (!ViewData.ModelState.IsValid)
{
<div class="mb-4 p-3 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm">
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
<p>@error.ErrorMessage</p>
}
</div>
}
<form method="post" class="space-y-4">
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" asp-for="ReturnUrl" />
<div class="space-y-2">
<label for="Username" class="text-sm font-medium">用户名</label>
<input type="text"
id="Username"
name="Username"
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="请输入用户名"
value="@Model.Username"
required
autocomplete="username">
</div>
<div class="space-y-2">
<label for="Email" class="text-sm font-medium">邮箱</label>
<input type="email"
id="Email"
name="Email"
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="请输入邮箱地址"
value="@Model.Email"
required
autocomplete="email">
</div>
<div class="space-y-2">
<label for="Password" class="text-sm font-medium">密码</label>
<input type="password"
id="Password"
name="Password"
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="请输入密码至少6个字符"
required
autocomplete="new-password">
</div>
<div class="space-y-2">
<label for="ConfirmPassword" class="text-sm font-medium">确认密码</label>
<input type="password"
id="ConfirmPassword"
name="ConfirmPassword"
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="请再次输入密码"
required
autocomplete="new-password">
</div>
<button type="submit"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-11 w-full px-8 shadow">
注册
</button>
</form>
</div>
<div class="mt-6 text-center text-sm">
<span class="text-muted-foreground">已有账号?</span>
<a href="/account/login?returnUrl=@Model.ReturnUrl" class="text-primary hover:underline ml-1">立即登录</a>
</div>
</div>
</div>

View File

@ -1,146 +0,0 @@
@model Fengling.AuthService.ViewModels.AuthorizeViewModel
@{
Layout = "_Layout";
ViewData["Title"] = "授权确认";
}
<div class="min-h-[calc(100vh-4rem)] flex items-center justify-center px-4 py-8">
<div class="w-full max-w-md">
<!-- Header -->
<div class="text-center mb-8">
<div class="inline-flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 mb-4">
<svg class="h-8 w-8 text-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
</div>
<h1 class="text-2xl font-bold">授权确认</h1>
<p class="text-muted-foreground mt-2">
<span class="text-primary font-medium">@Model.ApplicationName</span>
请求访问您的账户
</p>
</div>
<!-- Authorization Card -->
<div class="bg-card border border-border rounded-lg shadow-sm overflow-hidden">
<!-- App Info Section -->
<div class="p-6 border-b border-border">
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<div class="h-12 w-12 rounded-lg bg-primary flex items-center justify-center text-primary-foreground text-lg font-semibold">
@(Model.ApplicationName?.Substring(0, Math.Min(1, Model.ApplicationName.Length)).ToUpper() ?? "A")
</div>
</div>
<div class="flex-1 min-w-0">
<h2 class="font-semibold text-lg truncate">@Model.ApplicationName</h2>
<p class="text-sm text-muted-foreground mt-1">
该应用将获得以下权限:
</p>
</div>
</div>
</div>
<!-- Permissions Section -->
<div class="p-6">
<h3 class="text-sm font-medium text-foreground mb-4">请求的权限</h3>
<div class="space-y-3">
@if (Model.Scopes != null && Model.Scopes.Length > 0)
{
@foreach (var scope in Model.Scopes)
{
<div class="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
<svg class="h-5 w-5 text-primary mt-0.5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<div class="flex-1">
<p class="text-sm font-medium">@GetScopeDisplayName(scope)</p>
<p class="text-xs text-muted-foreground mt-0.5">@GetScopeDescription(scope)</p>
</div>
</div>
}
}
else
{
<p class="text-sm text-muted-foreground">无特定权限请求</p>
}
</div>
</div>
<!-- Warning Section -->
<div class="p-4 bg-destructive/5 border-t border-border">
<div class="flex items-start gap-2">
<svg class="h-4 w-4 text-destructive mt-0.5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/>
<path d="M12 9v4"/>
<path d="M12 17h.01"/>
</svg>
<p class="text-xs text-muted-foreground">
授予权限后,该应用将能够访问您的账户信息。您可以随时在授权管理中撤销权限。
</p>
</div>
</div>
</div>
<!-- Action Buttons -->
<form method="post" class="mt-6 space-y-3">
<div class="grid grid-cols-2 gap-3">
<button type="submit" name="action" value="accept"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-11 px-8 shadow">
<svg class="mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
授权
</button>
<button type="submit" name="action" value="deny"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-11 px-8">
<svg class="mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
取消
</button>
</div>
</form>
<!-- Footer Links -->
<div class="mt-6 text-center text-sm">
<a href="#" class="text-muted-foreground hover:text-foreground transition-colors">
了解关于 OAuth 授权的更多信息
</a>
</div>
</div>
</div>
@functions {
private string GetScopeDisplayName(string scope)
{
return scope switch
{
"openid" => "OpenID Connect",
"profile" => "个人资料",
"email" => "电子邮件地址",
"phone" => "电话号码",
"address" => "地址信息",
"roles" => "角色权限",
"offline_access" => "离线访问",
_ => scope
};
}
private string GetScopeDescription(string scope)
{
return scope switch
{
"openid" => "用于用户身份验证",
"profile" => "访问您的姓名、头像等基本信息",
"email" => "访问您的电子邮件地址",
"phone" => "访问您的电话号码",
"address" => "访问您的地址信息",
"roles" => "访问您的角色和权限信息",
"offline_access" => "在您离线时仍可访问数据",
_ => "自定义权限范围"
};
}
}

View File

@ -1,155 +0,0 @@
@model Fengling.AuthService.ViewModels.DashboardViewModel
@{
Layout = "_Layout";
ViewData["Title"] = "控制台";
}
<div class="container mx-auto px-4 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold">欢迎,@Model.Username</h1>
<p class="text-muted-foreground mt-2">这里是您的控制台首页</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-card border border-border rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">已登录应用</p>
<p class="text-2xl font-bold mt-2">3</p>
</div>
<div class="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
<svg class="h-6 w-6 text-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
</div>
</div>
</div>
<div class="bg-card border border-border rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">授权次数</p>
<p class="text-2xl font-bold mt-2">12</p>
</div>
<div class="h-12 w-12 rounded-full bg-green-500/10 flex items-center justify-center">
<svg class="h-6 w-6 text-green-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
</div>
</div>
</div>
<div class="bg-card border border-border rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">活跃会话</p>
<p class="text-2xl font-bold mt-2">5</p>
</div>
<div class="h-12 w-12 rounded-full bg-blue-500/10 flex items-center justify-center">
<svg class="h-6 w-6 text-blue-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
</div>
</div>
</div>
<div class="bg-card border border-border rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">安全评分</p>
<p class="text-2xl font-bold mt-2">92%</p>
</div>
<div class="h-12 w-12 rounded-full bg-yellow-500/10 flex items-center justify-center">
<svg class="h-6 w-6 text-yellow-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 bg-card border border-border rounded-lg p-6">
<h2 class="text-lg font-semibold mb-4">最近活动</h2>
<div class="space-y-4">
<div class="flex items-center gap-4 p-3 rounded-lg bg-muted/50">
<div class="h-10 w-10 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
F
</div>
<div class="flex-1">
<p class="text-sm font-medium">登录成功</p>
<p class="text-xs text-muted-foreground">通过 Fengling.Console.Web 登录</p>
</div>
<span class="text-xs text-muted-foreground">2分钟前</span>
</div>
<div class="flex items-center gap-4 p-3 rounded-lg bg-muted/50">
<div class="h-10 w-10 rounded-full bg-green-600 flex items-center justify-center text-white text-sm font-medium">
</div>
<div class="flex-1">
<p class="text-sm font-medium">授权成功</p>
<p class="text-xs text-muted-foreground">授予 Fengling.Console.Web 访问权限</p>
</div>
<span class="text-xs text-muted-foreground">5分钟前</span>
</div>
<div class="flex items-center gap-4 p-3 rounded-lg bg-muted/50">
<div class="h-10 w-10 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
🔄
</div>
<div class="flex-1">
<p class="text-sm font-medium">令牌刷新</p>
<p class="text-xs text-muted-foreground">刷新访问令牌</p>
</div>
<span class="text-xs text-muted-foreground">1小时前</span>
</div>
</div>
</div>
<div class="bg-card border border-border rounded-lg p-6">
<h2 class="text-lg font-semibold mb-4">快捷操作</h2>
<div class="space-y-3">
<a href="/profile" class="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-muted transition-colors">
<svg class="h-5 w-5 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span class="text-sm">个人资料</span>
</a>
<a href="/settings" class="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-muted transition-colors">
<svg class="h-5 w-5 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.1a2 2 0 0 1-1-1.72v-.51a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span class="text-sm">账户设置</span>
</a>
<a href="/connect/authorize" class="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-muted transition-colors">
<svg class="h-5 w-5 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<span class="text-sm">授权管理</span>
</a>
<form method="post" action="/account/logout">
<button type="submit" class="w-full flex items-center gap-3 p-3 rounded-lg border border-destructive/20 hover:bg-destructive/10 transition-colors text-left">
<svg class="h-5 w-5 text-destructive" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
<span class="text-sm text-destructive">退出登录</span>
</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,50 +0,0 @@
@model Fengling.AuthService.ViewModels.DashboardViewModel
@{
Layout = "_Layout";
ViewData["Title"] = "个人资料";
}
<div class="container mx-auto px-4 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold">个人资料</h1>
<p class="text-muted-foreground mt-2">管理您的个人信息</p>
</div>
<div class="max-w-2xl">
<div class="bg-card border border-border rounded-lg p-6">
<div class="flex items-center gap-6 mb-6">
<div class="h-24 w-24 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-3xl font-bold">
@(Model.Username?.Substring(0, 1).ToUpper() ?? "U")
</div>
<div>
<h2 class="text-xl font-semibold">@Model.Username</h2>
<p class="text-muted-foreground">@Model.Email</p>
</div>
</div>
<div class="space-y-4">
<div class="space-y-2">
<label class="text-sm font-medium">用户名</label>
<div class="p-3 rounded-md border border-input bg-muted/50 text-sm">
@Model.Username
</div>
</div>
<div class="space-y-2">
<label class="text-sm font-medium">邮箱</label>
<div class="p-3 rounded-md border border-input bg-muted/50 text-sm">
@Model.Email
</div>
</div>
<div class="space-y-2">
<label class="text-sm font-medium">注册时间</label>
<div class="p-3 rounded-md border border-input bg-muted/50 text-sm">
2026-01-15
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,90 +0,0 @@
@model Fengling.AuthService.ViewModels.DashboardViewModel
@{
Layout = "_Layout";
ViewData["Title"] = "设置";
}
<div class="container mx-auto px-4 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold">账户设置</h1>
<p class="text-muted-foreground mt-2">管理您的账户设置和偏好</p>
</div>
<div class="max-w-2xl space-y-6">
<div class="bg-card border border-border rounded-lg p-6">
<h2 class="text-lg font-semibold mb-4">修改密码</h2>
<form method="post" class="space-y-4">
<div class="space-y-2">
<label for="currentPassword" class="text-sm font-medium">当前密码</label>
<input type="password"
id="currentPassword"
name="currentPassword"
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="请输入当前密码">
</div>
<div class="space-y-2">
<label for="newPassword" class="text-sm font-medium">新密码</label>
<input type="password"
id="newPassword"
name="newPassword"
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="请输入新密码至少6个字符">
</div>
<div class="space-y-2">
<label for="confirmPassword" class="text-sm font-medium">确认新密码</label>
<input type="password"
id="confirmPassword"
name="confirmPassword"
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="请再次输入新密码">
</div>
<button type="submit"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-8">
修改密码
</button>
</form>
</div>
<div class="bg-card border border-border rounded-lg p-6">
<h2 class="text-lg font-semibold mb-4">安全选项</h2>
<div class="space-y-4">
<div class="flex items-center justify-between p-4 rounded-lg border border-border">
<div>
<p class="font-medium">两步验证</p>
<p class="text-sm text-muted-foreground">为您的账户添加额外的安全保护</p>
</div>
<button class="inline-flex items-center justify-center rounded-md text-sm font-medium border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 px-4">
启用
</button>
</div>
<div class="flex items-center justify-between p-4 rounded-lg border border-border">
<div>
<p class="font-medium">登录通知</p>
<p class="text-sm text-muted-foreground">当有新设备登录时发送通知</p>
</div>
<button class="inline-flex items-center justify-center rounded-md text-sm font-medium border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 px-4">
配置
</button>
</div>
</div>
</div>
<div class="bg-card border border-border rounded-lg p-6">
<h2 class="text-lg font-semibold mb-4">危险区域</h2>
<div class="flex items-center justify-between p-4 rounded-lg border border-destructive/20">
<div>
<p class="font-medium text-destructive">删除账户</p>
<p class="text-sm text-muted-foreground">永久删除您的账户和所有数据</p>
</div>
<button class="inline-flex items-center justify-center rounded-md text-sm font-medium bg-destructive text-destructive-foreground hover:bg-destructive/90 h-9 px-4">
删除账户
</button>
</div>
</div>
</div>
</div>

View File

@ -1,162 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Fengling 认证服务系统 - 提供安全可靠的身份认证和授权服务" />
<meta name="author" content="Fengling Team" />
<meta name="keywords" content="认证,授权,登录,注册,SSO,OAuth2" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>@ViewData["Title"] - Fengling Auth</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- shadcn Theme CSS -->
<link rel="stylesheet" href="~/css/styles.css" />
</head>
<body class="min-h-screen antialiased flex flex-col">
<!-- Header / Navigation -->
<header class="sticky top-0 z-50 w-full border-b border-border bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/60">
<div class="container mx-auto flex h-16 items-center justify-between px-4">
<!-- Logo and Brand -->
<div class="flex items-center gap-2">
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5"/>
<path d="M2 12l10 5 10-5"/>
</svg>
</div>
<span class="text-lg font-bold">Fengling Auth</span>
</div>
<!-- Navigation Links -->
<nav class="hidden md:flex items-center gap-6 text-sm font-medium">
<a href="/" class="transition-colors hover:text-foreground text-foreground">首页</a>
<a href="/dashboard" class="transition-colors hover:text-foreground text-muted-foreground">控制台</a>
<a href="/docs" class="transition-colors hover:text-foreground text-muted-foreground">文档</a>
<a href="/api" class="transition-colors hover:text-foreground text-muted-foreground">API</a>
</nav>
<!-- User Actions -->
<div class="flex items-center gap-4">
@if (User.Identity?.IsAuthenticated == true)
{
<!-- User Menu (Logged In) -->
<div class="relative group">
<button class="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-muted transition-colors">
<div class="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
@(User.Identity.Name?.Substring(0, 1).ToUpper() ?? "U")
</div>
<span class="hidden sm:inline">@User.Identity.Name</span>
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<!-- Dropdown Menu -->
<div class="absolute right-0 top-full mt-1 w-48 rounded-md border border-border bg-white shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all">
<div class="p-2">
<a href="/profile" class="block rounded-sm px-3 py-2 text-sm hover:bg-muted transition-colors">个人资料</a>
<a href="/settings" class="block rounded-sm px-3 py-2 text-sm hover:bg-muted transition-colors">设置</a>
<hr class="my-2 border-border">
<a href="/logout" class="block rounded-sm px-3 py-2 text-sm hover:bg-muted transition-colors text-red-600">退出登录</a>
</div>
</div>
</div>
}
else
{
<!-- Login/Register Buttons (Guest) -->
<a href="/login" class="text-sm font-medium hover:text-foreground text-muted-foreground transition-colors">登录</a>
<a href="/register" class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 py-2">
注册
</a>
}
</div>
</div>
</header>
<!-- Main Content -->
<main class="flex-1">
@RenderBody()
</main>
<!-- Footer -->
<footer class="border-t border-border bg-muted/50">
<div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Brand Column -->
<div class="space-y-3">
<div class="flex items-center gap-2">
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5"/>
<path d="M2 12l10 5 10-5"/>
</svg>
</div>
<span class="font-bold">Fengling Auth</span>
</div>
<p class="text-sm text-muted-foreground">
提供安全可靠的身份认证和授权服务,帮助企业快速实现用户管理和权限控制。
</p>
</div>
<!-- Product Column -->
<div class="space-y-3">
<h3 class="text-sm font-semibold">产品</h3>
<ul class="space-y-2 text-sm text-muted-foreground">
<li><a href="#" class="hover:text-foreground transition-colors">功能特性</a></li>
<li><a href="#" class="hover:text-foreground transition-colors">定价方案</a></li>
<li><a href="#" class="hover:text-foreground transition-colors">集成文档</a></li>
<li><a href="#" class="hover:text-foreground transition-colors">更新日志</a></li>
</ul>
</div>
<!-- Resources Column -->
<div class="space-y-3">
<h3 class="text-sm font-semibold">资源</h3>
<ul class="space-y-2 text-sm text-muted-foreground">
<li><a href="#" class="hover:text-foreground transition-colors">文档中心</a></li>
<li><a href="#" class="hover:text-foreground transition-colors">API 参考</a></li>
<li><a href="#" class="hover:text-foreground transition-colors">SDK 下载</a></li>
<li><a href="#" class="hover:text-foreground transition-colors">常见问题</a></li>
</ul>
</div>
<!-- Company Column -->
<div class="space-y-3">
<h3 class="text-sm font-semibold">关于</h3>
<ul class="space-y-2 text-sm text-muted-foreground">
<li><a href="#" class="hover:text-foreground transition-colors">关于我们</a></li>
<li><a href="#" class="hover:text-foreground transition-colors">联系方式</a></li>
<li><a href="#" class="hover:text-foreground transition-colors">隐私政策</a></li>
<li><a href="#" class="hover:text-foreground transition-colors">服务条款</a></li>
</ul>
</div>
</div>
<!-- Bottom Bar -->
<div class="mt-8 pt-8 border-t border-border">
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
<p class="text-sm text-muted-foreground">
© 2026 Fengling Team. All rights reserved.
</p>
<div class="flex items-center gap-4">
<a href="#" class="text-muted-foreground hover:text-foreground transition-colors">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
</a>
<a href="#" class="text-muted-foreground hover:text-foreground transition-colors">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
</div>
</div>
</div>
</div>
</footer>
</body>
</html>

View File

@ -1,3 +0,0 @@
@using Fengling.AuthService
@using Fengling.AuthService.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -1,3 +0,0 @@
@{
Layout = "_Layout";
}

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -1,22 +0,0 @@
{
"ConnectionStrings": {
"DefaultConnection": "DataSource=:memory:"
},
"Jwt": {
"Issuer": "https://auth.fengling.local",
"Audience": "fengling-api",
"Secret": "FenglingAuthSecretKey2024!ChangeThisInProduction!"
},
"OpenIddict": {
"Issuer": "https://auth.fengling.local",
"Audience": "fengling-api"
},
"Testing": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -1,21 +0,0 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542"
},
"Jwt": {
"Issuer": "https://auth.fengling.local",
"Audience": "fengling-api",
"Secret": "FenglingAuthSecretKey2024!ChangeThisInProduction!"
},
"OpenIddict": {
"Issuer": "https://auth.fengling.local",
"Audience": "fengling-api"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -1,210 +0,0 @@
/* ============================================
shadcn UI Theme Variables
Based on shadcn/ui default theme
============================================ */
/* Light Mode Variables */
:root {
/* Background & Foreground */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* Card */
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
/* Popover */
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
/* Primary */
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* Secondary */
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
/* Muted */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Accent */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Destructive */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
/* Borders & Inputs */
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
/* Ring */
--ring: 222.2 84% 4.9%;
/* Radius */
--radius: 0.5rem;
}
/* Dark Mode Variables */
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
/* ============================================
Base Styles
============================================ */
* {
border-color: hsl(var(--border));
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-feature-settings: "rlig" 1, "calt" 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ============================================
Utility Classes
============================================ */
/* Background colors */
.bg-primary {
background-color: hsl(var(--primary));
}
.bg-secondary {
background-color: hsl(var(--secondary));
}
.bg-muted {
background-color: hsl(var(--muted));
}
.bg-accent {
background-color: hsl(var(--accent));
}
.bg-destructive {
background-color: hsl(var(--destructive));
}
/* Text colors */
.text-primary-foreground {
color: hsl(var(--primary-foreground));
}
.text-secondary-foreground {
color: hsl(var(--secondary-foreground));
}
.text-muted-foreground {
color: hsl(var(--muted-foreground));
}
.text-accent-foreground {
color: hsl(var(--accent-foreground));
}
.text-destructive-foreground {
color: hsl(var(--destructive-foreground));
}
.text-foreground {
color: hsl(var(--foreground));
}
/* Borders */
.border-border {
border-color: hsl(var(--border));
}
/* Hover effects */
.hover\:bg-muted:hover {
background-color: hsl(var(--muted));
}
.hover\:bg-primary:hover {
background-color: hsl(var(--primary));
}
.hover\:text-foreground:hover {
color: hsl(var(--foreground));
}
/* ============================================
Transitions
============================================ */
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
/* ============================================
Component Styles
============================================ */
/* Button Styles */
.btn-primary {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.btn-primary:hover {
background-color: hsl(var(--primary) / 0.9);
}
/* Card Styles */
.card {
background-color: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
}
.card-foreground {
color: hsl(var(--card-foreground));
}
/* Input Styles */
.input {
background-color: hsl(var(--background));
border: 1px solid hsl(var(--input));
color: hsl(var(--foreground));
}
.input:focus {
outline: none;
border-color: hsl(var(--ring));
box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2);
}

View File

@ -1,193 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 风铃认证服务</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: white;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
width: 100%;
max-width: 400px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 24px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: none;
border-color: #667eea;
}
.remember-me {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.remember-me input {
margin-right: 8px;
}
button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
font-weight: 600;
}
button:hover {
opacity: 0.9;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error {
background: #fee;
color: #c33;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
font-size: 14px;
}
.loading {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 0.8s ease-in-out infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<h1>风铃认证服务</h1>
<div id="error-message" class="error" style="display: none;"></div>
<form id="login-form">
<input type="hidden" id="returnUrl" name="returnUrl">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required autofocus autocomplete="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<div class="remember-me">
<input type="checkbox" id="rememberMe" name="rememberMe">
<label for="rememberMe" style="margin-bottom: 0;">记住我</label>
</div>
<button type="submit" id="submit-btn">
<span id="btn-text">登录</span>
</button>
</form>
</div>
<script>
const form = document.getElementById('login-form');
const errorElement = document.getElementById('error-message');
const submitBtn = document.getElementById('submit-btn');
const btnText = document.getElementById('btn-text');
// Get returnUrl from query parameter
const urlParams = new URLSearchParams(window.location.search);
const returnUrl = urlParams.get('returnUrl') || '/';
document.getElementById('returnUrl').value = returnUrl;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const rememberMe = document.getElementById('rememberMe').checked;
// Show loading state
submitBtn.disabled = true;
btnText.innerHTML = '<span class="loading"></span>登录中...';
try {
const response = await fetch('/account/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
password: password,
rememberMe: rememberMe,
returnUrl: returnUrl
})
});
const data = await response.json();
if (response.ok) {
// Redirect to returnUrl
window.location.href = data.returnUrl;
} else {
// Show error
errorElement.textContent = data.error || '登录失败,请重试';
errorElement.style.display = 'block';
// Reset button
submitBtn.disabled = false;
btnText.textContent = '登录';
}
} catch (error) {
errorElement.textContent = '网络错误,请重试';
errorElement.style.display = 'block';
// Reset button
submitBtn.disabled = false;
btnText.textContent = '登录';
}
});
</script>
</body>
</html>

View File

@ -1,7 +0,0 @@
# Fengling.Console.Web 运管中心前端
## 开发环境配置
VITE_AUTH_SERVER_URL=http://localhost:5132
VITE_GATEWAY_SERVICE_URL=http://localhost:5001
VITE_CLIENT_ID=fengling-console
VITE_REDIRECT_URI=http://localhost:5173/auth/callback

View File

@ -1,7 +0,0 @@
# Fengling.Console.Web 运管中心前端
## 生产环境配置
VITE_AUTH_SERVER_URL=https://auth.fengling.local
VITE_GATEWAY_SERVICE_URL=https://gateway.fengling.local
VITE_CLIENT_ID=fengling-console
VITE_REDIRECT_URI=https://console.fengling.local/auth/callback

View File

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,139 +0,0 @@
# Fengling Console.Web - 风铃运管中心前端
## 项目概述
统一运维管理平台前端用于管理网关配置、OAuth应用、用户等。
## 技术栈
- Vue 3.4
- TypeScript
- Vite 5.4
- Element Plus
- Pinia
- Vue Router 4
- Axios
## 功能模块
### 1. 认证模块
- OAuth2 授权码流登录
- 密码流登录
- Token 自动刷新
- 登出功能
### 2. 网关管理
- 租户列表和管理
- 租户路由配置
- 集群实例管理
- 全局路由配置
- 负载均衡策略
### 3. OAuth 应用管理
- OAuth Client CRUD
- 重定向 URI 配置
- 授权类型配置
- Client 状态管理
### 4. 用户管理
- 用户列表
- 角色分配
- 租户分配
- 用户状态管理
## 环境变量
### 开发环境 (.env.development)
```env
VITE_AUTH_SERVICE_URL=http://localhost:5000
VITE_GATEWAY_SERVICE_URL=http://localhost:5001
VITE_CLIENT_ID=fengling-console
VITE_REDIRECT_URI=http://localhost:5173/auth/callback
```
### 生产环境 (.env.production)
```env
VITE_AUTH_SERVICE_URL=https://auth.fengling.local
VITE_GATEWAY_SERVICE_URL=https://gateway.fengling.local
```
## 开发指南
### 安装依赖
```bash
npm install
```
### 启动开发服务器
```bash
npm run dev
```
访问: http://localhost:5173
### 构建生产版本
```bash
npm run build
```
### 预览生产构建
```bash
npm run preview
```
## 目录结构
```
src/
├── api/
│ ├── auth.ts # AuthService API封装
│ └── gateway.ts # Gateway API封装
├── components/ # 公共组件
├── stores/
│ └── auth.ts # Pinia store for auth
├── router/
│ └── index.ts # Vue Router配置
└── views/
├── Auth/ # 认证相关页面
│ ├── Login.vue
│ └── Callback.vue
├── Gateway/ # 网关管理页面
│ └── Dashboard.vue
├── OAuth/ # OAuth应用管理
│ └── ClientList.vue
└── Users/ # 用户管理
└── UserList.vue
```
## API代理配置
Vite 开发服务器配置了以下 API 代理:
- `/api/auth/*``http://localhost:5000` (AuthService)
- `/api/gateway/*``http://localhost:5001` (YarpGateway)
## OAuth2 集成
Client 已在 AuthService 的 SeedData 中预注册:
```yaml
Client ID: fengling-console
授权类型: authorization_code, refresh_token
重定向 URI: http://console.fengling.local/auth/callback
作用域: api, offline_access
```
## 后续开发
### 迁移任务
- [ ] 迁移 TenantList.vue
- [ ] 迁移 TenantRoutes.vue
- [ ] 迁移 ClusterInstances.vue
- [ ] 迁移 GlobalRoutes.vue
### 功能增强
- [ ] Token 自动刷新拦截器
- [ ] 请求错误统一处理
- [ ] Loading 状态管理
- [ ] 权限控制
- [ ] 完善 OAuth2 授权码流

View File

@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>fengling-console-web</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
{
"name": "fengling-console-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"oidc-client-ts": "^3.4.1",
"vue": "^3.5.24"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"typescript": "~5.9.3",
"vite": "^7.2.4",
"vue-tsc": "^3.1.4"
}
}

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Silent Renew</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/oidc-client-ts@3.4.1/dist/umd/oidc-client-ts.min.js"></script>
<script>
const userManager = new oidc.UserManager({
authority: window.location.origin,
client_id: 'fengling-console',
redirect_uri: window.location.origin + '/auth/callback',
silent_redirect_uri: window.location.origin + '/silent-renew.html',
response_type: 'code',
scope: 'openid profile api offline_access',
automaticSilentRenew: true,
})
userManager.signinSilentCallback().catch(function (err) {
console.error('Silent renew error:', err)
})
</script>
</body>
</html>

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,25 +0,0 @@
<script setup lang="ts">
</script>
<template>
<router-view />
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
font-size: 14px;
color: #333;
background-color: #fff;
}
</style>

View File

@ -1,88 +0,0 @@
import axios from 'axios'
const gatewayAxios = axios.create({
baseURL: '/api/gateway',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
gatewayAxios.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
export const gatewayService = {
async getTenants() {
const response = await gatewayAxios.get('/tenants')
return response.data
},
async getTenant(id: number) {
const response = await gatewayAxios.get(`/tenants/${id}`)
return response.data
},
async createTenant(tenant: any) {
const response = await gatewayAxios.post('/tenants', tenant)
return response.data
},
async updateTenant(id: number, tenant: any) {
const response = await gatewayAxios.put(`/tenants/${id}`, tenant)
return response.data
},
async deleteTenant(id: number) {
await gatewayAxios.delete(`/tenants/${id}`)
},
async getTenantRoutes(tenantId: number) {
const response = await gatewayAxios.get(`/tenants/${tenantId}/routes`)
return response.data
},
async createTenantRoute(tenantId: number, route: any) {
const response = await gatewayAxios.post(`/tenants/${tenantId}/routes`, route)
return response.data
},
async updateTenantRoute(id: number, route: any) {
const response = await gatewayAxios.put(`/routes/${id}`, route)
return response.data
},
async deleteTenantRoute(id: number) {
await gatewayAxios.delete(`/routes/${id}`)
},
async getClusterInstances(clusterId: number) {
const response = await gatewayAxios.get(`/clusters/${clusterId}/instances`)
return response.data
},
async createClusterInstance(clusterId: number, instance: any) {
const response = await gatewayAxios.post(`/clusters/${clusterId}/instances`, instance)
return response.data
},
async updateClusterInstance(id: number, instance: any) {
const response = await gatewayAxios.put(`/instances/${id}`, instance)
return response.data
},
async deleteClusterInstance(id: number) {
await gatewayAxios.delete(`/instances/${id}`)
},
}
export default gatewayAxios

View File

@ -1,49 +0,0 @@
import axios from 'axios'
const api = axios.create({
baseURL: '/api',
timeout: 30000,
})
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
try {
const { useAuthStore } = await import('@/stores/auth')
const authStore = useAuthStore()
await authStore.refresh()
originalRequest.headers.Authorization = `Bearer ${localStorage.getItem('access_token')}`
return api(originalRequest)
} catch (refreshError) {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
localStorage.removeItem('user_info')
window.location.href = '/login'
return Promise.reject(error)
}
}
return Promise.reject(error)
}
)
export default api

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

View File

@ -1,41 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -1,15 +0,0 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')

Some files were not shown because too many files have changed in this diff Show More