Initial commit: MengStack Go API framework
Clean public release with: - Go/Gin/PostgreSQL/Redis architecture - Multi-tenant support - JWT authentication - Docker Compose deployment - Environment-based configuration
29
.env.example
Normal file
@ -0,0 +1,29 @@
|
||||
# MengStack Environment Configuration
|
||||
# Copy to .env and fill in actual values
|
||||
|
||||
# Server
|
||||
MENGSTACK_SERVER_PORT=2222
|
||||
MENGSTACK_SERVER_MODE=debug
|
||||
|
||||
# PostgreSQL
|
||||
MENGSTACK_DATABASE_HOST=127.0.0.1
|
||||
MENGSTACK_DATABASE_PORT=5432
|
||||
MENGSTACK_DATABASE_USER=postgres
|
||||
MENGSTACK_DATABASE_PASSWORD=
|
||||
MENGSTACK_DATABASE_DBNAME=mengstack
|
||||
MENGSTACK_DATABASE_SSLMODE=disable
|
||||
|
||||
# Redis
|
||||
MENGSTACK_REDIS_ADDR=127.0.0.1:6379
|
||||
MENGSTACK_REDIS_PASSWORD=
|
||||
MENGSTACK_REDIS_DB=0
|
||||
|
||||
# JWT (MUST change in production)
|
||||
MENGSTACK_JWT_SECRET=change-me-in-production
|
||||
MENGSTACK_JWT_ACCESS_EXPIRY_MINUTES=30
|
||||
MENGSTACK_JWT_REFRESH_EXPIRY_DAYS=7
|
||||
MENGSTACK_JWT_ISSUER=mengstack
|
||||
|
||||
# Logging
|
||||
MENGSTACK_LOG_LEVEL=debug
|
||||
MENGSTACK_LOG_FORMAT=console
|
||||
46
.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
# Binaries
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
build/
|
||||
bin/
|
||||
|
||||
# Build output
|
||||
/dist/
|
||||
|
||||
# Environment - NEVER commit real credentials
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Config files with real credentials
|
||||
configs/config.local.yaml
|
||||
configs/config.*.local.yaml
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Uploads
|
||||
/uploads/*
|
||||
!/uploads/.gitkeep
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Test
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
# Swagger generated (regenerate with make swagger)
|
||||
# docs/docs.go
|
||||
# docs/swagger.json
|
||||
# docs/swagger.yaml
|
||||
41
Makefile
Normal file
@ -0,0 +1,41 @@
|
||||
.PHONY: build run test clean lint dev swagger
|
||||
|
||||
APP_NAME=mengstack
|
||||
BUILD_DIR=build
|
||||
|
||||
build:
|
||||
go build -o $(BUILD_DIR)/$(APP_NAME) ./cmd/server
|
||||
|
||||
run:
|
||||
go run ./cmd/server
|
||||
|
||||
dev:
|
||||
go run ./cmd/server
|
||||
|
||||
test:
|
||||
go test ./... -v -race
|
||||
|
||||
test-cover:
|
||||
go test ./... -v -race -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
swagger:
|
||||
swag init -g cmd/server/main.go -o docs --parseDependency --parseInternal
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR) coverage.out coverage.html
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
docker-up:
|
||||
docker-compose up -d
|
||||
|
||||
docker-down:
|
||||
docker-compose down
|
||||
|
||||
docker-logs:
|
||||
docker-compose logs -f
|
||||
BIN
assets/logo/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/logo/favicon-16.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/logo/favicon-32.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
assets/logo/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/logo/horizontal-source-cropped.png
Normal file
|
After Width: | Height: | Size: 832 KiB |
BIN
assets/logo/icon-source-cropped.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
assets/logo/logo-hero-200.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/logo/logo-hero-300.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
assets/logo/logo-icon-128.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/logo/logo-icon-48.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
assets/logo/logo-icon-512.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
assets/logo/logo-icon-64.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/logo/logo-icon-dark-512.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
assets/logo/logo-icon-transparent-512.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
assets/logo/logo-login-280w.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/logo/logo-nav-32h.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/logo/logo-nav-dark-32h.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/logo/logo-nav-light-32h.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
assets/logo/logo-readme-1000w.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
assets/logo/logo-readme-600w.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
assets/logo/logo-sidebar-40h.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
assets/logo/og-image-1200x630.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
29
cmd/server/main.go
Normal file
@ -0,0 +1,29 @@
|
||||
// MengStack API
|
||||
//
|
||||
// @title MengStack API
|
||||
// @version 0.1.0
|
||||
// @description MengStack 平台 API 文档
|
||||
//
|
||||
// @host localhost:2222
|
||||
// @BasePath /api/v1
|
||||
//
|
||||
// @securityDefinitions.apiKey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Bearer <token>
|
||||
//
|
||||
// @securityDefinitions.apiKey TenantID
|
||||
// @in header
|
||||
// @name X-Tenant-ID
|
||||
// @description 租户 ID(受保护接口必填)
|
||||
package main
|
||||
|
||||
import (
|
||||
"mengstack/internal/app"
|
||||
|
||||
_ "mengstack/docs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.NewApp().Run()
|
||||
}
|
||||
6
configs/config.dev.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
server:
|
||||
mode: debug
|
||||
|
||||
log:
|
||||
level: debug
|
||||
format: console
|
||||
6
configs/config.prod.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
server:
|
||||
mode: release
|
||||
|
||||
log:
|
||||
level: info
|
||||
format: json
|
||||
26
configs/config.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
server:
|
||||
port: 2222
|
||||
mode: debug
|
||||
|
||||
database:
|
||||
host: 127.0.0.1
|
||||
port: 5432
|
||||
user: postgres
|
||||
password: ""
|
||||
dbname: mengstack
|
||||
sslmode: disable
|
||||
|
||||
redis:
|
||||
addr: 127.0.0.1:6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
jwt:
|
||||
secret: change-me-in-production
|
||||
access_expiry_minutes: 30
|
||||
refresh_expiry_days: 7
|
||||
issuer: mengstack
|
||||
|
||||
log:
|
||||
level: info
|
||||
format: json
|
||||
59
docker-compose.yml
Normal file
@ -0,0 +1,59 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-mengstack}
|
||||
POSTGRES_USER: ${DB_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-mengstack_dev}
|
||||
ports:
|
||||
- "${DB_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
api:
|
||||
build: .
|
||||
ports:
|
||||
- "${SERVER_PORT:-2222}:2222"
|
||||
environment:
|
||||
MENGSTACK_SERVER_PORT: ${SERVER_PORT:-2222}
|
||||
MENGSTACK_SERVER_MODE: ${SERVER_MODE:-debug}
|
||||
MENGSTACK_DATABASE_HOST: postgres
|
||||
MENGSTACK_DATABASE_PORT: 5432
|
||||
MENGSTACK_DATABASE_USER: ${DB_USER:-postgres}
|
||||
MENGSTACK_DATABASE_PASSWORD: ${DB_PASSWORD:-mengstack_dev}
|
||||
MENGSTACK_DATABASE_DBNAME: ${DB_NAME:-mengstack}
|
||||
MENGSTACK_DATABASE_SSLMODE: disable
|
||||
MENGSTACK_REDIS_ADDR: redis:6379
|
||||
MENGSTACK_REDIS_PASSWORD: ${REDIS_PASSWORD:-}
|
||||
MENGSTACK_REDIS_DB: ${REDIS_DB:-0}
|
||||
MENGSTACK_JWT_SECRET: ${JWT_SECRET:-change-me-in-production}
|
||||
MENGSTACK_JWT_ACCESS_EXPIRY_MINUTES: 30
|
||||
MENGSTACK_JWT_REFRESH_EXPIRY_DAYS: 7
|
||||
MENGSTACK_JWT_ISSUER: mengstack
|
||||
MENGSTACK_LOG_LEVEL: ${LOG_LEVEL:-debug}
|
||||
MENGSTACK_LOG_FORMAT: ${LOG_FORMAT:-console}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
514
docs/docs.go
Normal file
@ -0,0 +1,514 @@
|
||||
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"description": "使用邮箱和密码登录,返回 JWT token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "用户登录",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "登录信息",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.LoginRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/refresh": {
|
||||
"post": {
|
||||
"description": "使用 refresh_token 获取新的 token 对",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "刷新 Token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "{ \\",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"description": "使用邮箱、用户名和密码注册新用户,返回 JWT token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "用户注册",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "注册信息",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.RegisterRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"description": "检查 PostgreSQL 和 Redis 连接状态",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "健康检查",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"database": {
|
||||
"type": "string"
|
||||
},
|
||||
"redis": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "integer"
|
||||
},
|
||||
"trace_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/password": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"TenantID": []
|
||||
}
|
||||
],
|
||||
"description": "使用旧密码验证后设置新密码",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "修改密码",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "密码修改请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.ChangePasswordRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ping": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"TenantID": []
|
||||
}
|
||||
],
|
||||
"description": "返回 pong,用于验证认证和多租户中间件是否正常",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "健康探测",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/profile": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"TenantID": []
|
||||
}
|
||||
],
|
||||
"description": "返回当前认证用户的个人资料",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "获取当前用户信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.UserDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"mengstack_internal_kernel_response.Response": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"trace_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.ChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_password",
|
||||
"old_password"
|
||||
],
|
||||
"properties": {
|
||||
"new_password": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 8
|
||||
},
|
||||
"old_password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.LoginRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.RegisterRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password",
|
||||
"username"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"nickname": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 8
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.TokenPair": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"expires_in": {
|
||||
"type": "integer"
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.UserDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"nickname": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"Bearer": {
|
||||
"description": "Bearer \u003ctoken\u003e",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
},
|
||||
"TenantID": {
|
||||
"description": "租户 ID(受保护接口必填)",
|
||||
"type": "apiKey",
|
||||
"name": "X-Tenant-ID",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "0.1.0",
|
||||
Host: "localhost:2222",
|
||||
BasePath: "/api/v1",
|
||||
Schemes: []string{},
|
||||
Title: "MengStack API",
|
||||
Description: "MengStack 平台 API 文档",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
||||
490
docs/swagger.json
Normal file
@ -0,0 +1,490 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "MengStack 平台 API 文档",
|
||||
"title": "MengStack API",
|
||||
"contact": {},
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"host": "localhost:2222",
|
||||
"basePath": "/api/v1",
|
||||
"paths": {
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"description": "使用邮箱和密码登录,返回 JWT token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "用户登录",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "登录信息",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.LoginRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/refresh": {
|
||||
"post": {
|
||||
"description": "使用 refresh_token 获取新的 token 对",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "刷新 Token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "{ \\",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"description": "使用邮箱、用户名和密码注册新用户,返回 JWT token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "用户注册",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "注册信息",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.RegisterRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.TokenPair"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"description": "检查 PostgreSQL 和 Redis 连接状态",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "健康检查",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"database": {
|
||||
"type": "string"
|
||||
},
|
||||
"redis": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "integer"
|
||||
},
|
||||
"trace_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/password": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"TenantID": []
|
||||
}
|
||||
],
|
||||
"description": "使用旧密码验证后设置新密码",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "修改密码",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "密码修改请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.ChangePasswordRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ping": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"TenantID": []
|
||||
}
|
||||
],
|
||||
"description": "返回 pong,用于验证认证和多租户中间件是否正常",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "健康探测",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/profile": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"TenantID": []
|
||||
}
|
||||
],
|
||||
"description": "返回当前认证用户的个人资料",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "获取当前用户信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/mengstack_internal_modules_auth_domain.UserDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/mengstack_internal_kernel_response.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"mengstack_internal_kernel_response.Response": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"trace_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.ChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_password",
|
||||
"old_password"
|
||||
],
|
||||
"properties": {
|
||||
"new_password": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 8
|
||||
},
|
||||
"old_password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.LoginRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.RegisterRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password",
|
||||
"username"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"nickname": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 8
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"minLength": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.TokenPair": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"expires_in": {
|
||||
"type": "integer"
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mengstack_internal_modules_auth_domain.UserDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"nickname": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"Bearer": {
|
||||
"description": "Bearer \u003ctoken\u003e",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
},
|
||||
"TenantID": {
|
||||
"description": "租户 ID(受保护接口必填)",
|
||||
"type": "apiKey",
|
||||
"name": "X-Tenant-ID",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
307
docs/swagger.yaml
Normal file
@ -0,0 +1,307 @@
|
||||
basePath: /api/v1
|
||||
definitions:
|
||||
mengstack_internal_kernel_response.Response:
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
data: {}
|
||||
message:
|
||||
type: string
|
||||
trace_id:
|
||||
type: string
|
||||
type: object
|
||||
mengstack_internal_modules_auth_domain.ChangePasswordRequest:
|
||||
properties:
|
||||
new_password:
|
||||
maxLength: 128
|
||||
minLength: 8
|
||||
type: string
|
||||
old_password:
|
||||
type: string
|
||||
required:
|
||||
- new_password
|
||||
- old_password
|
||||
type: object
|
||||
mengstack_internal_modules_auth_domain.LoginRequest:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
type: object
|
||||
mengstack_internal_modules_auth_domain.RegisterRequest:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
nickname:
|
||||
type: string
|
||||
password:
|
||||
maxLength: 128
|
||||
minLength: 8
|
||||
type: string
|
||||
username:
|
||||
maxLength: 64
|
||||
minLength: 3
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
- username
|
||||
type: object
|
||||
mengstack_internal_modules_auth_domain.TokenPair:
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
expires_in:
|
||||
type: integer
|
||||
refresh_token:
|
||||
type: string
|
||||
type: object
|
||||
mengstack_internal_modules_auth_domain.UserDTO:
|
||||
properties:
|
||||
avatar:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
nickname:
|
||||
type: string
|
||||
status:
|
||||
type: integer
|
||||
tenant_id:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
host: localhost:2222
|
||||
info:
|
||||
contact: {}
|
||||
description: MengStack 平台 API 文档
|
||||
title: MengStack API
|
||||
version: 0.1.0
|
||||
paths:
|
||||
/auth/login:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用邮箱和密码登录,返回 JWT token
|
||||
parameters:
|
||||
- description: 登录信息
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_modules_auth_domain.LoginRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/mengstack_internal_modules_auth_domain.TokenPair'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
summary: 用户登录
|
||||
tags:
|
||||
- Auth
|
||||
/auth/refresh:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用 refresh_token 获取新的 token 对
|
||||
parameters:
|
||||
- description: '{ \'
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/mengstack_internal_modules_auth_domain.TokenPair'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
summary: 刷新 Token
|
||||
tags:
|
||||
- Auth
|
||||
/auth/register:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用邮箱、用户名和密码注册新用户,返回 JWT token
|
||||
parameters:
|
||||
- description: 注册信息
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_modules_auth_domain.RegisterRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/mengstack_internal_modules_auth_domain.TokenPair'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
"409":
|
||||
description: Conflict
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
summary: 用户注册
|
||||
tags:
|
||||
- Auth
|
||||
/health:
|
||||
get:
|
||||
description: 检查 PostgreSQL 和 Redis 连接状态
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
properties:
|
||||
database:
|
||||
type: string
|
||||
redis:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
timestamp:
|
||||
type: integer
|
||||
trace_id:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
summary: 健康检查
|
||||
tags:
|
||||
- System
|
||||
/password:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用旧密码验证后设置新密码
|
||||
parameters:
|
||||
- description: 密码修改请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_modules_auth_domain.ChangePasswordRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
security:
|
||||
- Bearer: []
|
||||
- TenantID: []
|
||||
summary: 修改密码
|
||||
tags:
|
||||
- Auth
|
||||
/ping:
|
||||
get:
|
||||
description: 返回 pong,用于验证认证和多租户中间件是否正常
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
security:
|
||||
- Bearer: []
|
||||
- TenantID: []
|
||||
summary: 健康探测
|
||||
tags:
|
||||
- System
|
||||
/profile:
|
||||
get:
|
||||
description: 返回当前认证用户的个人资料
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/mengstack_internal_modules_auth_domain.UserDTO'
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/mengstack_internal_kernel_response.Response'
|
||||
security:
|
||||
- Bearer: []
|
||||
- TenantID: []
|
||||
summary: 获取当前用户信息
|
||||
tags:
|
||||
- Auth
|
||||
securityDefinitions:
|
||||
Bearer:
|
||||
description: Bearer <token>
|
||||
in: header
|
||||
name: Authorization
|
||||
type: apiKey
|
||||
TenantID:
|
||||
description: 租户 ID(受保护接口必填)
|
||||
in: header
|
||||
name: X-Tenant-ID
|
||||
type: apiKey
|
||||
swagger: "2.0"
|
||||
86
go.mod
Normal file
@ -0,0 +1,86 @@
|
||||
module mengstack
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/spf13/viper v1.19.0
|
||||
go.uber.org/fx v1.23.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
gorm.io/driver/postgres v1.5.9
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/dig v1.18.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
243
go.sum
Normal file
@ -0,0 +1,243 @@
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
|
||||
go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
|
||||
go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
102
internal/app/app.go
Normal file
@ -0,0 +1,102 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"mengstack/internal/app/cache"
|
||||
"mengstack/internal/app/database"
|
||||
"mengstack/internal/app/health"
|
||||
"mengstack/internal/app/middleware"
|
||||
"mengstack/internal/config"
|
||||
"mengstack/internal/logger"
|
||||
authinterfaces "mengstack/internal/modules/auth/interfaces"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
var Module = fx.Module("app",
|
||||
fx.Provide(newEngine),
|
||||
fx.Provide(newServer),
|
||||
fx.Invoke(registerLifecycle),
|
||||
)
|
||||
|
||||
func newEngine(
|
||||
cfg *config.Config,
|
||||
log *zap.Logger,
|
||||
db *gorm.DB,
|
||||
rdb *redis.Client,
|
||||
authHandler *authinterfaces.Handler,
|
||||
authMW gin.HandlerFunc,
|
||||
) *gin.Engine {
|
||||
ginMode := "release"
|
||||
if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" {
|
||||
ginMode = "debug"
|
||||
}
|
||||
gin.SetMode(ginMode)
|
||||
r := gin.New()
|
||||
|
||||
r.Use(middleware.RequestID())
|
||||
r.Use(middleware.RequestLogger())
|
||||
r.Use(middleware.CORS())
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
healthHandler := health.NewHandler(db, rdb, log)
|
||||
r.GET("/health", healthHandler.Handle)
|
||||
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
authinterfaces.SetupRoutes(r, authHandler, authMW)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func newServer(cfg *config.Config, engine *gin.Engine) *http.Server {
|
||||
return &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
|
||||
Handler: engine,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func registerLifecycle(lc fx.Lifecycle, srv *http.Server, log *zap.Logger) {
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
log.Info("server starting", zap.String("addr", srv.Addr))
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Error("server error", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
log.Info("server shutting down")
|
||||
return srv.Shutdown(ctx)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func NewApp() *fx.App {
|
||||
return fx.New(
|
||||
fx.Provide(
|
||||
func() (*config.Config, error) { return config.Load() },
|
||||
func(cfg *config.Config) (*zap.Logger, error) { return logger.New(cfg.Log) },
|
||||
),
|
||||
database.Module,
|
||||
cache.Module,
|
||||
authinterfaces.Module,
|
||||
Module,
|
||||
)
|
||||
}
|
||||
14
internal/app/cache/module.go
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"mengstack/internal/config"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var Module = fx.Module("cache",
|
||||
fx.Provide(func(cfg *config.Config) (*redis.Client, error) {
|
||||
return NewRedis(cfg.Redis)
|
||||
}),
|
||||
)
|
||||
24
internal/app/cache/redis.go
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mengstack/internal/config"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func NewRedis(cfg config.RedisConfig) (*redis.Client, error) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Addr,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
})
|
||||
|
||||
if err := rdb.Ping(context.Background()).Err(); err != nil {
|
||||
return nil, fmt.Errorf("connect to Redis: %w", err)
|
||||
}
|
||||
|
||||
return rdb, nil
|
||||
}
|
||||
14
internal/app/database/module.go
Normal file
@ -0,0 +1,14 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"mengstack/internal/config"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var Module = fx.Module("database",
|
||||
fx.Provide(func(cfg *config.Config) (*gorm.DB, error) {
|
||||
return NewPostgres(cfg.Database)
|
||||
}),
|
||||
)
|
||||
37
internal/app/database/postgres.go
Normal file
@ -0,0 +1,37 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"mengstack/internal/config"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func NewPostgres(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
57
internal/app/health/handler.go
Normal file
@ -0,0 +1,57 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
db *gorm.DB
|
||||
rdb *redis.Client
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewHandler(db *gorm.DB, rdb *redis.Client, log *zap.Logger) *Handler {
|
||||
return &Handler{db: db, rdb: rdb, log: log}
|
||||
}
|
||||
|
||||
// Handle godoc
|
||||
// @Summary 健康检查
|
||||
// @Description 检查 PostgreSQL 和 Redis 连接状态
|
||||
// @Tags System
|
||||
// @Produce json
|
||||
// @Success 200 {object} object{status=string,database=string,redis=string,timestamp=integer,version=string,trace_id=string}
|
||||
// @Router /health [get]
|
||||
func (h *Handler) Handle(c *gin.Context) {
|
||||
status := "ok"
|
||||
dbStatus := "connected"
|
||||
redisStatus := "connected"
|
||||
|
||||
sqlDB, err := h.db.DB()
|
||||
if err != nil || sqlDB.Ping() != nil {
|
||||
status = "degraded"
|
||||
dbStatus = "disconnected"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
if h.rdb.Ping(ctx).Err() != nil {
|
||||
status = "degraded"
|
||||
redisStatus = "disconnected"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": status,
|
||||
"database": dbStatus,
|
||||
"redis": redisStatus,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"version": "0.1.0",
|
||||
"trace_id": c.GetString("trace_id"),
|
||||
})
|
||||
}
|
||||
38
internal/app/middleware/requestid.go
Normal file
@ -0,0 +1,38 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func RequestID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
traceID := c.GetHeader("X-Request-ID")
|
||||
if traceID == "" {
|
||||
traceID = uuid.New().String()
|
||||
}
|
||||
c.Set("trace_id", traceID)
|
||||
c.Header("X-Request-ID", traceID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RequestLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
|
||||
c.Next()
|
||||
|
||||
latency := time.Since(start)
|
||||
status := c.Writer.Status()
|
||||
gin.DefaultWriter.Write([]byte(
|
||||
"[" + c.GetString("trace_id") + "] " +
|
||||
c.Request.Method + " " + path + " " +
|
||||
strconv.Itoa(status) + " " + latency.String() + "\n",
|
||||
))
|
||||
}
|
||||
}
|
||||
63
internal/app/middleware/tenant.go
Normal file
@ -0,0 +1,63 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"mengstack/internal/kernel/response"
|
||||
"mengstack/internal/kernel/errors"
|
||||
"mengstack/internal/kernel/tenant"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func MultiTenant() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tenantID := c.GetHeader("X-Tenant-ID")
|
||||
if tenantID == "" {
|
||||
tenantID = c.Query("tenant_id")
|
||||
}
|
||||
if tenantID == "" {
|
||||
response.Fail(c, errors.ErrTenantRequired)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("tenant_id", tenantID)
|
||||
ctx := tenant.WithTenantID(c.Request.Context(), tenantID)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func OptionalTenant() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tenantID := c.GetHeader("X-Tenant-ID")
|
||||
if tenantID == "" {
|
||||
tenantID = c.Query("tenant_id")
|
||||
}
|
||||
if tenantID != "" {
|
||||
c.Set("tenant_id", tenantID)
|
||||
ctx := tenant.WithTenantID(c.Request.Context(), tenantID)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CORS() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, X-Tenant-ID, X-Request-ID")
|
||||
c.Header("Access-Control-Expose-Headers", "X-Request-ID")
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
107
internal/config/config.go
Normal file
@ -0,0 +1,107 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Mode string `mapstructure:"mode"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
DBName string `mapstructure:"dbname"`
|
||||
SSLMode string `mapstructure:"sslmode"`
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Addr string `mapstructure:"addr"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string `mapstructure:"secret"`
|
||||
AccessExpiryMinutes int `mapstructure:"access_expiry_minutes"`
|
||||
RefreshExpiryDays int `mapstructure:"refresh_expiry_days"`
|
||||
Issuer string `mapstructure:"issuer"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
v := viper.New()
|
||||
v.SetConfigName("config")
|
||||
v.SetConfigType("yaml")
|
||||
v.AddConfigPath("./configs/")
|
||||
v.AddConfigPath(".")
|
||||
|
||||
v.SetEnvPrefix("MENGSTACK")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
|
||||
setDefaults(v)
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
|
||||
// Check env var for mode override before merging env config
|
||||
env := v.GetString("server.mode")
|
||||
if envOverride := os.Getenv("MENGSTACK_SERVER_MODE"); envOverride != "" {
|
||||
env = envOverride
|
||||
}
|
||||
mergeEnvConfig(v, env)
|
||||
|
||||
var cfg Config
|
||||
if err := v.Unmarshal(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("server.port", 8080)
|
||||
v.SetDefault("server.mode", "debug")
|
||||
v.SetDefault("database.host", "127.0.0.1")
|
||||
v.SetDefault("database.port", 5432)
|
||||
v.SetDefault("database.user", "postgres")
|
||||
v.SetDefault("database.dbname", "mengstack")
|
||||
v.SetDefault("database.sslmode", "disable")
|
||||
v.SetDefault("redis.addr", "127.0.0.1:6379")
|
||||
v.SetDefault("redis.db", 0)
|
||||
v.SetDefault("jwt.access_expiry_minutes", 30)
|
||||
v.SetDefault("jwt.refresh_expiry_days", 7)
|
||||
v.SetDefault("jwt.issuer", "mengstack")
|
||||
v.SetDefault("log.level", "info")
|
||||
v.SetDefault("log.format", "json")
|
||||
}
|
||||
|
||||
func mergeEnvConfig(v *viper.Viper, env string) {
|
||||
envFile := fmt.Sprintf("config.%s", env)
|
||||
v.SetConfigName(envFile)
|
||||
if err := v.MergeInConfig(); err != nil {
|
||||
// environment-specific config is optional
|
||||
}
|
||||
v.SetConfigName("config")
|
||||
}
|
||||
57
internal/kernel/errors/errors.go
Normal file
@ -0,0 +1,57 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AppError struct {
|
||||
Code string
|
||||
Message string
|
||||
HTTPCode int
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
|
||||
}
|
||||
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (e *AppError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
func New(code, message string, httpCode int) *AppError {
|
||||
return &AppError{Code: code, Message: message, HTTPCode: httpCode}
|
||||
}
|
||||
|
||||
func Wrap(cause error, code, message string, httpCode int) *AppError {
|
||||
return &AppError{Code: code, Message: message, HTTPCode: httpCode, Cause: cause}
|
||||
}
|
||||
|
||||
func IsAppError(err error) (*AppError, bool) {
|
||||
var appErr *AppError
|
||||
if errors.As(err, &appErr) {
|
||||
return appErr, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidParam = New("INVALID_PARAM", "invalid parameter", http.StatusBadRequest)
|
||||
ErrUnauthorized = New("UNAUTHORIZED", "unauthorized", http.StatusUnauthorized)
|
||||
ErrForbidden = New("FORBIDDEN", "forbidden", http.StatusForbidden)
|
||||
ErrNotFound = New("NOT_FOUND", "resource not found", http.StatusNotFound)
|
||||
ErrConflict = New("CONFLICT", "resource already exists", http.StatusConflict)
|
||||
ErrInternal = New("INTERNAL_ERROR", "internal server error", http.StatusInternalServerError)
|
||||
ErrTenantRequired = New("TENANT_REQUIRED", "tenant context required", http.StatusBadRequest)
|
||||
ErrTokenExpired = New("TOKEN_EXPIRED", "token expired", http.StatusUnauthorized)
|
||||
ErrTokenInvalid = New("TOKEN_INVALID", "invalid token", http.StatusUnauthorized)
|
||||
ErrPasswordWrong = New("PASSWORD_WRONG", "wrong password", http.StatusUnauthorized)
|
||||
ErrUserNotFound = New("USER_NOT_FOUND", "user not found", http.StatusNotFound)
|
||||
ErrUserExists = New("USER_EXISTS", "user already exists", http.StatusConflict)
|
||||
ErrRefreshToken = New("REFRESH_TOKEN_INVALID", "invalid refresh token", http.StatusUnauthorized)
|
||||
)
|
||||
35
internal/kernel/model/base.go
Normal file
@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BaseModel struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
TenantID string `json:"tenant_id" gorm:"type:uuid;not null;index;primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
CreatorID uint `json:"creator_id"`
|
||||
}
|
||||
|
||||
type Tenant struct {
|
||||
ID string `json:"id" gorm:"type:uuid;primaryKey"`
|
||||
Name string `json:"name" gorm:"size:128;not null"`
|
||||
Slug string `json:"slug" gorm:"uniqueIndex;size:64;not null"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
func (Tenant) TableName() string {
|
||||
return "tenants"
|
||||
}
|
||||
|
||||
func NewTenantID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
53
internal/kernel/response/response.go
Normal file
@ -0,0 +1,53 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"mengstack/internal/kernel/errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
TraceID string `json:"trace_id"`
|
||||
}
|
||||
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
TraceID: c.GetString("trace_id"),
|
||||
})
|
||||
}
|
||||
|
||||
func Fail(c *gin.Context, err *errors.AppError) {
|
||||
c.JSON(err.HTTPCode, Response{
|
||||
Code: -1,
|
||||
Message: err.Message,
|
||||
TraceID: c.GetString("trace_id"),
|
||||
})
|
||||
}
|
||||
|
||||
func FailWithMessage(c *gin.Context, err *errors.AppError, message string) {
|
||||
c.JSON(err.HTTPCode, Response{
|
||||
Code: -1,
|
||||
Message: message,
|
||||
TraceID: c.GetString("trace_id"),
|
||||
})
|
||||
}
|
||||
|
||||
func HandleError(c *gin.Context, err error) {
|
||||
if appErr, ok := errors.IsAppError(err); ok {
|
||||
Fail(c, appErr)
|
||||
return
|
||||
}
|
||||
Fail(c, errors.ErrInternal)
|
||||
}
|
||||
|
||||
func NewBadRequest(msg string) *errors.AppError {
|
||||
return errors.New("BAD_REQUEST", msg, http.StatusBadRequest)
|
||||
}
|
||||
21
internal/kernel/tenant/context.go
Normal file
@ -0,0 +1,21 @@
|
||||
package tenant
|
||||
|
||||
import "context"
|
||||
|
||||
type ctxKey string
|
||||
|
||||
const tenantIDKey ctxKey = "tenant_id"
|
||||
|
||||
func WithTenantID(ctx context.Context, tenantID string) context.Context {
|
||||
return context.WithValue(ctx, tenantIDKey, tenantID)
|
||||
}
|
||||
|
||||
func FromContext(ctx context.Context) (string, bool) {
|
||||
id, ok := ctx.Value(tenantIDKey).(string)
|
||||
return id, ok && id != ""
|
||||
}
|
||||
|
||||
func MustFromContext(ctx context.Context) string {
|
||||
id, _ := FromContext(ctx)
|
||||
return id
|
||||
}
|
||||
34
internal/logger/logger.go
Normal file
@ -0,0 +1,34 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"mengstack/internal/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func New(cfg config.LogConfig) (*zap.Logger, error) {
|
||||
var level zapcore.Level
|
||||
if err := level.UnmarshalText([]byte(cfg.Level)); err != nil {
|
||||
level = zapcore.InfoLevel
|
||||
}
|
||||
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoderConfig.TimeKey = "time"
|
||||
encoderConfig.MessageKey = "msg"
|
||||
encoderConfig.LevelKey = "level"
|
||||
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
encoderConfig.EncodeLevel = zapcore.LowercaseLevelEncoder
|
||||
|
||||
var encoder zapcore.Encoder
|
||||
if cfg.Format == "console" {
|
||||
encoder = zapcore.NewConsoleEncoder(encoderConfig)
|
||||
} else {
|
||||
encoder = zapcore.NewJSONEncoder(encoderConfig)
|
||||
}
|
||||
|
||||
core := zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), level)
|
||||
return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)), nil
|
||||
}
|
||||
46
internal/modules/auth/application/jwt.go
Normal file
@ -0,0 +1,46 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Type string `json:"type"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(userID uint, tenantID, tokenType string, expiry time.Duration, secret, issuer string) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
TenantID: tenantID,
|
||||
Type: tokenType,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiry)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: issuer,
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
func ParseToken(tokenString, secret string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, jwt.ErrTokenInvalidClaims
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
177
internal/modules/auth/application/service.go
Normal file
@ -0,0 +1,177 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"mengstack/internal/kernel/errors"
|
||||
"mengstack/internal/kernel/tenant"
|
||||
"mengstack/internal/modules/auth/domain"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo domain.UserRepository
|
||||
jwtCfg JWTConfig
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string
|
||||
AccessExpiryMinutes int
|
||||
RefreshExpiryDays int
|
||||
Issuer string
|
||||
}
|
||||
|
||||
func NewService(repo domain.UserRepository, jwtCfg JWTConfig) *Service {
|
||||
return &Service{repo: repo, jwtCfg: jwtCfg}
|
||||
}
|
||||
|
||||
func (s *Service) Register(ctx context.Context, req domain.RegisterRequest) (domain.TokenPair, error) {
|
||||
tenantID, ok := tenant.FromContext(ctx)
|
||||
if !ok {
|
||||
return domain.TokenPair{}, errors.ErrTenantRequired
|
||||
}
|
||||
|
||||
if _, err := s.repo.FindByEmail(ctx, tenantID, req.Email); err == nil {
|
||||
return domain.TokenPair{}, errors.ErrUserExists
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)
|
||||
if err != nil {
|
||||
return domain.TokenPair{}, errors.Wrap(err, "HASH_FAILED", "failed to hash password", 500)
|
||||
}
|
||||
|
||||
user := &domain.User{
|
||||
TenantID: tenantID,
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: string(hash),
|
||||
Nickname: req.Nickname,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(ctx, user); err != nil {
|
||||
return domain.TokenPair{}, err
|
||||
}
|
||||
|
||||
return s.generateTokens(user)
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, req domain.LoginRequest) (domain.TokenPair, error) {
|
||||
tenantID, ok := tenant.FromContext(ctx)
|
||||
if !ok {
|
||||
return domain.TokenPair{}, errors.ErrTenantRequired
|
||||
}
|
||||
|
||||
user, err := s.repo.FindByEmail(ctx, tenantID, req.Email)
|
||||
if err != nil {
|
||||
return domain.TokenPair{}, errors.ErrUnauthorized
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
||||
return domain.TokenPair{}, errors.ErrPasswordWrong
|
||||
}
|
||||
|
||||
if user.Status != 1 {
|
||||
return domain.TokenPair{}, errors.ErrForbidden
|
||||
}
|
||||
|
||||
tokens, err := s.generateTokens(user)
|
||||
if err != nil {
|
||||
return domain.TokenPair{}, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
user.LastLogin = &now
|
||||
_ = s.repo.Update(ctx, user)
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (domain.TokenPair, error) {
|
||||
claims, err := ParseToken(refreshToken, s.jwtCfg.Secret)
|
||||
if err != nil {
|
||||
return domain.TokenPair{}, errors.ErrRefreshToken
|
||||
}
|
||||
if claims.Type != "refresh" {
|
||||
return domain.TokenPair{}, errors.ErrRefreshToken
|
||||
}
|
||||
|
||||
tenantID, ok := tenant.FromContext(ctx)
|
||||
if !ok {
|
||||
return domain.TokenPair{}, errors.ErrTenantRequired
|
||||
}
|
||||
user, err := s.repo.FindByID(ctx, tenantID, claims.UserID)
|
||||
if err != nil {
|
||||
return domain.TokenPair{}, errors.ErrUnauthorized
|
||||
}
|
||||
|
||||
return s.generateTokens(user)
|
||||
}
|
||||
|
||||
func (s *Service) ChangePassword(ctx context.Context, userID uint, req domain.ChangePasswordRequest) error {
|
||||
tenantID, ok := tenant.FromContext(ctx)
|
||||
if !ok {
|
||||
return errors.ErrTenantRequired
|
||||
}
|
||||
|
||||
user, err := s.repo.FindByID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return errors.ErrUserNotFound
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
|
||||
return errors.ErrPasswordWrong
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), 12)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "HASH_FAILED", "failed to hash password", 500)
|
||||
}
|
||||
|
||||
user.Password = string(hash)
|
||||
return s.repo.Update(ctx, user)
|
||||
}
|
||||
|
||||
func (s *Service) GetProfile(ctx context.Context, userID uint) (domain.UserDTO, error) {
|
||||
tenantID, ok := tenant.FromContext(ctx)
|
||||
if !ok {
|
||||
return domain.UserDTO{}, errors.ErrTenantRequired
|
||||
}
|
||||
|
||||
user, err := s.repo.FindByID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return domain.UserDTO{}, errors.ErrUserNotFound
|
||||
}
|
||||
|
||||
return domain.ToUserDTO(user), nil
|
||||
}
|
||||
|
||||
func (s *Service) JWTSecret() string {
|
||||
return s.jwtCfg.Secret
|
||||
}
|
||||
|
||||
func (s *Service) generateTokens(user *domain.User) (domain.TokenPair, error) {
|
||||
access, err := GenerateToken(user.ID, user.TenantID, "access",
|
||||
time.Duration(s.jwtCfg.AccessExpiryMinutes)*time.Minute,
|
||||
s.jwtCfg.Secret, s.jwtCfg.Issuer,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.TokenPair{}, errors.Wrap(err, "TOKEN_GEN_FAILED", "failed to generate token", 500)
|
||||
}
|
||||
|
||||
refresh, err := GenerateToken(user.ID, user.TenantID, "refresh",
|
||||
time.Duration(s.jwtCfg.RefreshExpiryDays)*24*time.Hour,
|
||||
s.jwtCfg.Secret, s.jwtCfg.Issuer,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.TokenPair{}, errors.Wrap(err, "TOKEN_GEN_FAILED", "failed to generate refresh token", 500)
|
||||
}
|
||||
|
||||
return domain.TokenPair{
|
||||
AccessToken: access,
|
||||
RefreshToken: refresh,
|
||||
ExpiresIn: s.jwtCfg.AccessExpiryMinutes * 60,
|
||||
}, nil
|
||||
}
|
||||
46
internal/modules/auth/domain/dto.go
Normal file
@ -0,0 +1,46 @@
|
||||
package domain
|
||||
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=64"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=8,max=128"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=8,max=128"`
|
||||
}
|
||||
|
||||
type TokenPair struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type UserDTO struct {
|
||||
ID uint `json:"id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func ToUserDTO(u *User) UserDTO {
|
||||
return UserDTO{
|
||||
ID: u.ID,
|
||||
TenantID: u.TenantID,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
Nickname: u.Nickname,
|
||||
Avatar: u.Avatar,
|
||||
Status: u.Status,
|
||||
}
|
||||
}
|
||||
11
internal/modules/auth/domain/repository.go
Normal file
@ -0,0 +1,11 @@
|
||||
package domain
|
||||
|
||||
import "context"
|
||||
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, user *User) error
|
||||
FindByID(ctx context.Context, tenantID string, id uint) (*User, error)
|
||||
FindByEmail(ctx context.Context, tenantID string, email string) (*User, error)
|
||||
FindByUsername(ctx context.Context, tenantID string, username string) (*User, error)
|
||||
Update(ctx context.Context, user *User) error
|
||||
}
|
||||
21
internal/modules/auth/domain/user.go
Normal file
@ -0,0 +1,21 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
TenantID string `json:"tenant_id" gorm:"type:uuid;not null;index:idx_tenant_email,unique;primaryKey"`
|
||||
Username string `json:"username" gorm:"uniqueIndex;size:64;not null"`
|
||||
Email string `json:"email" gorm:"index:idx_tenant_email,unique;size:128;not null"`
|
||||
Password string `json:"-" gorm:"size:256;not null"`
|
||||
Nickname string `json:"nickname" gorm:"size:64"`
|
||||
Avatar string `json:"avatar" gorm:"size:512"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
11
internal/modules/auth/infrastructure/migrate.go
Normal file
@ -0,0 +1,11 @@
|
||||
package infrastructure
|
||||
|
||||
import (
|
||||
"mengstack/internal/modules/auth/domain"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Migrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(&domain.User{})
|
||||
}
|
||||
52
internal/modules/auth/infrastructure/user_repo.go
Normal file
@ -0,0 +1,52 @@
|
||||
package infrastructure
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"mengstack/internal/modules/auth/domain"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type userRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) domain.UserRepository {
|
||||
return &userRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *userRepo) Create(ctx context.Context, user *domain.User) error {
|
||||
return r.db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
func (r *userRepo) FindByID(ctx context.Context, tenantID string, id uint) (*domain.User, error) {
|
||||
var user domain.User
|
||||
err := r.db.WithContext(ctx).Where("id = ? AND tenant_id = ?", id, tenantID).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepo) FindByEmail(ctx context.Context, tenantID string, email string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
err := r.db.WithContext(ctx).Where("email = ? AND tenant_id = ?", email, tenantID).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepo) FindByUsername(ctx context.Context, tenantID string, username string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
err := r.db.WithContext(ctx).Where("username = ? AND tenant_id = ?", username, tenantID).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepo) Update(ctx context.Context, user *domain.User) error {
|
||||
return r.db.WithContext(ctx).Save(user).Error
|
||||
}
|
||||
166
internal/modules/auth/interfaces/handler.go
Normal file
@ -0,0 +1,166 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"mengstack/internal/kernel/response"
|
||||
"mengstack/internal/modules/auth/application"
|
||||
"mengstack/internal/modules/auth/domain"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
svc *application.Service
|
||||
}
|
||||
|
||||
func NewHandler(svc *application.Service) *Handler {
|
||||
return &Handler{svc: svc}
|
||||
}
|
||||
|
||||
// Register godoc
|
||||
// @Summary 用户注册
|
||||
// @Description 使用邮箱、用户名和密码注册新用户,返回 JWT token
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.RegisterRequest true "注册信息"
|
||||
// @Success 200 {object} response.Response{data=domain.TokenPair}
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 409 {object} response.Response
|
||||
// @Router /auth/register [post]
|
||||
func (h *Handler) Register(c *gin.Context) {
|
||||
var req domain.RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := h.svc.Register(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, tokens)
|
||||
}
|
||||
|
||||
// Login godoc
|
||||
// @Summary 用户登录
|
||||
// @Description 使用邮箱和密码登录,返回 JWT token
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.LoginRequest true "登录信息"
|
||||
// @Success 200 {object} response.Response{data=domain.TokenPair}
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 401 {object} response.Response
|
||||
// @Router /auth/login [post]
|
||||
func (h *Handler) Login(c *gin.Context) {
|
||||
var req domain.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := h.svc.Login(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, tokens)
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
// @Summary 刷新 Token
|
||||
// @Description 使用 refresh_token 获取新的 token 对
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body object true "{ \"refresh_token\": \"...\" }"
|
||||
// @Success 200 {object} response.Response{data=domain.TokenPair}
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 401 {object} response.Response
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *Handler) RefreshToken(c *gin.Context) {
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := h.svc.RefreshToken(c.Request.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, tokens)
|
||||
}
|
||||
|
||||
// ChangePassword godoc
|
||||
// @Summary 修改密码
|
||||
// @Description 使用旧密码验证后设置新密码
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.ChangePasswordRequest true "密码修改请求"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Failure 401 {object} response.Response
|
||||
// @Security Bearer
|
||||
// @Security TenantID
|
||||
// @Router /password [post]
|
||||
func (h *Handler) ChangePassword(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req domain.ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.ChangePassword(c.Request.Context(), userID, req); err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
// GetProfile godoc
|
||||
// @Summary 获取当前用户信息
|
||||
// @Description 返回当前认证用户的个人资料
|
||||
// @Tags Auth
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=domain.UserDTO}
|
||||
// @Failure 401 {object} response.Response
|
||||
// @Failure 404 {object} response.Response
|
||||
// @Security Bearer
|
||||
// @Security TenantID
|
||||
// @Router /profile [get]
|
||||
func (h *Handler) GetProfile(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
dto, err := h.svc.GetProfile(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
response.HandleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto)
|
||||
}
|
||||
|
||||
// Ping godoc
|
||||
// @Summary 健康探测
|
||||
// @Description 返回 pong,用于验证认证和多租户中间件是否正常
|
||||
// @Tags System
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security Bearer
|
||||
// @Security TenantID
|
||||
// @Router /ping [get]
|
||||
func (h *Handler) Ping(c *gin.Context) {
|
||||
response.Success(c, gin.H{"message": "pong"})
|
||||
}
|
||||
83
internal/modules/auth/interfaces/routes.go
Normal file
@ -0,0 +1,83 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"mengstack/internal/app/middleware"
|
||||
"mengstack/internal/config"
|
||||
"mengstack/internal/modules/auth/application"
|
||||
"mengstack/internal/modules/auth/infrastructure"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthMiddleware struct {
|
||||
fx.In
|
||||
|
||||
Service *application.Service
|
||||
}
|
||||
|
||||
func NewAuthMiddleware(in AuthMiddleware) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(401, gin.H{"code": -1, "message": "missing authorization"})
|
||||
return
|
||||
}
|
||||
|
||||
token := authHeader
|
||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
token = authHeader[7:]
|
||||
}
|
||||
|
||||
claims, err := application.ParseToken(token, in.Service.JWTSecret())
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(401, gin.H{"code": -1, "message": "invalid token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("tenant_id", claims.TenantID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func SetupRoutes(r *gin.Engine, h *Handler, authMW gin.HandlerFunc) {
|
||||
auth := r.Group("/api/v1/auth")
|
||||
auth.Use(middleware.OptionalTenant())
|
||||
{
|
||||
auth.POST("/register", h.Register)
|
||||
auth.POST("/login", h.Login)
|
||||
auth.POST("/refresh", h.RefreshToken)
|
||||
}
|
||||
|
||||
protected := r.Group("/api/v1")
|
||||
protected.Use(authMW, middleware.MultiTenant())
|
||||
{
|
||||
protected.GET("/ping", h.Ping)
|
||||
protected.GET("/profile", h.GetProfile)
|
||||
protected.POST("/password", h.ChangePassword)
|
||||
}
|
||||
}
|
||||
|
||||
func NewJWTConfig(cfg *config.Config) application.JWTConfig {
|
||||
return application.JWTConfig{
|
||||
Secret: cfg.JWT.Secret,
|
||||
AccessExpiryMinutes: cfg.JWT.AccessExpiryMinutes,
|
||||
RefreshExpiryDays: cfg.JWT.RefreshExpiryDays,
|
||||
Issuer: cfg.JWT.Issuer,
|
||||
}
|
||||
}
|
||||
|
||||
var Module = fx.Module("auth",
|
||||
fx.Provide(
|
||||
NewJWTConfig,
|
||||
infrastructure.NewUserRepository,
|
||||
application.NewService,
|
||||
NewHandler,
|
||||
NewAuthMiddleware,
|
||||
),
|
||||
fx.Invoke(func(db *gorm.DB) error {
|
||||
return infrastructure.Migrate(db)
|
||||
}),
|
||||
)
|
||||
2
migrations/000001_init_schema.down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS tenants;
|
||||
37
migrations/000001_init_schema.up.sql
Normal file
@ -0,0 +1,37 @@
|
||||
-- Initial schema: tenants + users
|
||||
CREATE TABLE IF NOT EXISTS tenants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(128) NOT NULL,
|
||||
slug VARCHAR(64) NOT NULL UNIQUE,
|
||||
status INT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tenants_deleted_at ON tenants(deleted_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL,
|
||||
tenant_id UUID NOT NULL,
|
||||
username VARCHAR(64) NOT NULL,
|
||||
email VARCHAR(128) NOT NULL,
|
||||
password VARCHAR(256) NOT NULL,
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
avatar VARCHAR(512) NOT NULL DEFAULT '',
|
||||
status INT NOT NULL DEFAULT 1,
|
||||
last_login TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (id, tenant_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tenant_email ON users(tenant_id, email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
||||
|
||||
-- Seed: default tenant
|
||||
INSERT INTO tenants (id, name, slug, status)
|
||||
VALUES ('00000000-0000-0000-0000-000000000001', 'Default Tenant', 'default', 1)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||