feat(console): add ASP.NET Core Identity dependency
This commit is contained in:
parent
29357dbf10
commit
b8a76dfd93
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"isRoot": true,
|
|
||||||
"tools": {
|
|
||||||
"csharpier": {
|
|
||||||
"version": "1.2.5",
|
|
||||||
"commands": [
|
|
||||||
"csharpier"
|
|
||||||
],
|
|
||||||
"rollForward": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
130
.gitignore
vendored
130
.gitignore
vendored
@ -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
|
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"deepscan.enable": true
|
|
||||||
}
|
|
||||||
@ -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
25
Fengling.Console.csproj
Normal 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>
|
||||||
@ -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
|
|
||||||
@ -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 | 审计日志 |
|
|
||||||
288
OpenCode.md
288
OpenCode.md
@ -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
370
README.md
@ -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
|
|
||||||
@ -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
@ -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
|
|
||||||
@ -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或使用同域代理
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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)
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
1421
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
89
sql/init.sql
89
sql/init.sql
@ -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;
|
|
||||||
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
bin/
|
|
||||||
obj/
|
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
*.md
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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("/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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("/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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"]
|
|
||||||
@ -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>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
@Fengling.AuthService_HostAddress = http://localhost:5132
|
|
||||||
|
|
||||||
GET {{Fengling.AuthService_HostAddress}}/weatherforecast/
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
@ -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>();
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
namespace Fengling.AuthService.ViewModels;
|
|
||||||
|
|
||||||
public record AuthorizeViewModel(string? ApplicationName, string? Scope)
|
|
||||||
{
|
|
||||||
public string[]? Scopes => Scope?.Split(' ') ?? null;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
namespace Fengling.AuthService.ViewModels;
|
|
||||||
|
|
||||||
public class DashboardViewModel
|
|
||||||
{
|
|
||||||
public string? Username { get; set; }
|
|
||||||
public string? Email { get; set; }
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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" => "在您离线时仍可访问数据",
|
|
||||||
_ => "自定义权限范围"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
@using Fengling.AuthService
|
|
||||||
@using Fengling.AuthService.ViewModels
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
@{
|
|
||||||
Layout = "_Layout";
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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": "*"
|
|
||||||
}
|
|
||||||
@ -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": "*"
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
24
src/Fengling.Console.Web/.gitignore
vendored
24
src/Fengling.Console.Web/.gitignore
vendored
@ -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?
|
|
||||||
@ -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 授权码流
|
|
||||||
@ -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>
|
|
||||||
1500
src/Fengling.Console.Web/package-lock.json
generated
1500
src/Fengling.Console.Web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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 |
@ -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>
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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 |
@ -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>
|
|
||||||
@ -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
Loading…
Reference in New Issue
Block a user