Compare commits
53 Commits
49d59eccb0
..
old
| Author | SHA1 | Date | |
|---|---|---|---|
| 615b87ef26 | |||
| d94cbc09b0 | |||
| 0e7cbf4396 | |||
| 5d632608ab | |||
| 338d962618 | |||
| f1794b26b1 | |||
| 4cd24e7600 | |||
| e18809f9b7 | |||
| 143445a2fc | |||
| 7277438b07 | |||
| 5c2d0be8e2 | |||
| 429e535dd4 | |||
| b29e5c8cb5 | |||
| c4166cef6b | |||
| e3999ba75f | |||
| 8a4cfcbdb3 | |||
| c8b3683cf2 | |||
| 1172ecc2f1 | |||
| 750a33cca2 | |||
| d0aad68bb0 | |||
| cb5bf5aae3 | |||
| 87b743a2a2 | |||
| 2305562a7c | |||
| eb25095623 | |||
| a9c98b67f1 | |||
| 2ed379d5e8 | |||
| d8e2e81688 | |||
| a5abbc1dd3 | |||
| 528ed63a55 | |||
| 81748abce4 | |||
| aa5e472184 | |||
| a15e08d8f3 | |||
| 6db4c1b826 | |||
| 396862f59d | |||
| 2dae837323 | |||
| 1b76794251 | |||
| 8b48286365 | |||
| 643d234a88 | |||
| eacf675671 | |||
| 96808d531a | |||
| adbe9a06d8 | |||
| 8562734fde | |||
| de4386ff68 | |||
| a273f8d883 | |||
| 6dcd75a39b | |||
| 29dd54546d | |||
| 41d8de42c0 | |||
| b97e2d650e | |||
| c5be0081ca | |||
| 1a6cced814 | |||
| bf24e1756a | |||
| 4e7a6e9e41 | |||
| 6c7d45a640 |
@@ -1,231 +0,0 @@
|
||||
name: Build docker and publish
|
||||
run-name: 简化的Docker构建和部署流程
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
env:
|
||||
# Docker镜像仓库
|
||||
REPO: ${{ vars.REPO || 'registry.kxsw.us/ppanel-server' }}
|
||||
# SSH连接信息
|
||||
SSH_HOST: ${{ vars.SSH_HOST }}
|
||||
SSH_PORT: ${{ vars.SSH_PORT }}
|
||||
SSH_USER: ${{ vars.SSH_USER }}
|
||||
SSH_PASSWORD: ${{ vars.SSH_PASSWORD }}
|
||||
# TG通知
|
||||
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
|
||||
TG_CHAT_ID: "-4940243803"
|
||||
# Go构建变量
|
||||
SERVICE: ppanel
|
||||
SERVICE_STYLE: ppanel
|
||||
VERSION: ${{ github.sha }}
|
||||
BUILDTIME: ${{ github.event.head_commit.timestamp }}
|
||||
GOARCH: amd64
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ppanel-server
|
||||
container:
|
||||
image: node:20
|
||||
strategy:
|
||||
matrix:
|
||||
# 只有node支持版本号别名
|
||||
node: ['20.15.1']
|
||||
steps:
|
||||
# 步骤1: 下载代码
|
||||
- name: 📥 下载代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 步骤2: 设置动态环境变量
|
||||
- name: ⚙️ 设置动态环境变量
|
||||
run: |
|
||||
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||
echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV
|
||||
echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV
|
||||
echo "DEPLOY_PATH=/root/vpn_server" >> $GITHUB_ENV
|
||||
echo "为 main 分支设置生产环境变量"
|
||||
elif [ "${{ github.ref_name }}" = "dev" ]; then
|
||||
echo "DOCKER_TAG_SUFFIX=dev" >> $GITHUB_ENV
|
||||
echo "CONTAINER_NAME=ppanel-server-dev" >> $GITHUB_ENV
|
||||
echo "DEPLOY_PATH=/root/vpn_server_dev" >> $GITHUB_ENV
|
||||
echo "为 dev 分支设置开发环境变量"
|
||||
else
|
||||
echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "CONTAINER_NAME=ppanel-server-${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "DEPLOY_PATH=/root/vpn_server_other" >> $GITHUB_ENV
|
||||
echo "为其他分支 (${{ github.ref_name }}) 设置环境变量"
|
||||
fi
|
||||
|
||||
# 步骤3: 安装系统工具 (Docker, curl, jq)
|
||||
- name: 🔧 安装系统工具 (Docker, curl, jq)
|
||||
run: |
|
||||
set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
echo "等待 apt/dpkg 锁释放 (unattended-upgrades)..."
|
||||
# 等待最多 300 秒让 unattended-upgrades/apt/dpkg 锁释放
|
||||
end=$((SECONDS+300))
|
||||
while true; do
|
||||
LOCKS_BUSY=0
|
||||
# 如果 unattended-upgrades 正在运行,标记为忙碌
|
||||
if pgrep -x unattended-upgrades >/dev/null 2>&1; then LOCKS_BUSY=1; fi
|
||||
# 如果 fuser 存在,检查常见的锁文件
|
||||
if command -v fuser >/dev/null 2>&1; then
|
||||
if fuser /var/lib/dpkg/lock >/dev/null 2>&1 \
|
||||
|| fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|
||||
|| fuser /var/lib/apt/lists/lock >/dev/null 2>&1; then
|
||||
LOCKS_BUSY=1
|
||||
fi
|
||||
fi
|
||||
# 如果不忙碌则跳出循环
|
||||
if [ "$LOCKS_BUSY" -eq 0 ]; then break; fi
|
||||
# 超时后约 5 分钟
|
||||
if [ $SECONDS -ge $end ]; then
|
||||
echo "等待 apt/dpkg 锁超时,使用 Dpkg::Lock::Timeout 继续..."
|
||||
break
|
||||
fi
|
||||
echo "仍在等待锁释放..."; sleep 5
|
||||
done
|
||||
apt-get update -y -o Dpkg::Lock::Timeout=600
|
||||
apt-get install -y -o Dpkg::Lock::Timeout=600 jq curl ca-certificates docker.io
|
||||
docker --version
|
||||
jq --version
|
||||
curl --version
|
||||
|
||||
# 步骤4: 构建并发布到镜像仓库
|
||||
- name: 📤 构建并发布到镜像仓库
|
||||
run: |
|
||||
echo "开始构建并推送镜像..."
|
||||
echo "仓库: ${{ env.REPO }}"
|
||||
echo "版本标签: ${{ env.VERSION }}"
|
||||
echo "分支标签: ${{ env.DOCKER_TAG_SUFFIX }}"
|
||||
|
||||
# 构建镜像,同时打上版本和分支两个标签
|
||||
docker build -f Dockerfile \
|
||||
--platform linux/amd64 \
|
||||
--build-arg TARGETARCH=amd64 \
|
||||
--build-arg VERSION=${{ env.VERSION }} \
|
||||
--build-arg BUILDTIME=${{ env.BUILDTIME }} \
|
||||
-t ${{ env.REPO }}:${{ env.VERSION }} \
|
||||
-t ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }} \
|
||||
.
|
||||
|
||||
echo "推送版本标签镜像: ${{ env.REPO }}:${{ env.VERSION }}"
|
||||
docker push ${{ env.REPO }}:${{ env.VERSION }}
|
||||
|
||||
echo "推送分支标签镜像: ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}"
|
||||
docker push ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}
|
||||
|
||||
echo "镜像推送完成"
|
||||
|
||||
# 步骤5: 连接服务器拉镜像启动
|
||||
- name: 🚀 连接服务器拉镜像启动
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ env.SSH_HOST }}
|
||||
username: ${{ env.SSH_USER }}
|
||||
password: ${{ env.SSH_PASSWORD }}
|
||||
port: ${{ env.SSH_PORT }}
|
||||
timeout: 300s
|
||||
command_timeout: 600s
|
||||
script: |
|
||||
echo "连接服务器成功,开始部署..."
|
||||
echo "部署容器名: ${{ env.CONTAINER_NAME }}"
|
||||
echo "部署路径: ${{ env.DEPLOY_PATH }}"
|
||||
echo "部署镜像: ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}"
|
||||
|
||||
# 确保部署目录存在
|
||||
mkdir -p ${{ env.DEPLOY_PATH }}/config
|
||||
mkdir -p ${{ env.DEPLOY_PATH }}/logs
|
||||
|
||||
# 停止并删除旧容器(忽略所有错误)
|
||||
if docker ps -a | grep -q ${{ env.CONTAINER_NAME }} 2>/dev/null; then
|
||||
echo "停止旧容器..."
|
||||
docker stop ${{ env.CONTAINER_NAME }} >/dev/null 2>&1 || true
|
||||
echo "等待容器完全停止..."
|
||||
sleep 5
|
||||
|
||||
echo "删除旧容器..."
|
||||
# 静默删除,完全忽略错误输出
|
||||
docker rm ${{ env.CONTAINER_NAME }} >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
|
||||
# 如果仍然存在,尝试强制删除(静默)
|
||||
if docker ps -a | grep -q ${{ env.CONTAINER_NAME }} 2>/dev/null; then
|
||||
echo "尝试强制删除..."
|
||||
docker rm -f ${{ env.CONTAINER_NAME }} >/dev/null 2>&1 || true
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
echo "容器清理完成,继续部署..."
|
||||
fi
|
||||
|
||||
# 拉取最新分支镜像
|
||||
echo "拉取镜像: ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}..."
|
||||
docker pull ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}
|
||||
|
||||
# 启动新容器
|
||||
echo "启动新容器..."
|
||||
cd ${{ env.DEPLOY_PATH }}
|
||||
docker run -d \
|
||||
--name ${{ env.CONTAINER_NAME }} \
|
||||
--restart unless-stopped \
|
||||
--network host \
|
||||
-v ./config/ppanel.yaml:/app/etc/ppanel.yaml \
|
||||
-v ./logs:/app/logs \
|
||||
${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}
|
||||
|
||||
# 检查容器状态
|
||||
sleep 5
|
||||
if docker ps | grep -q ${{ env.CONTAINER_NAME }}; then
|
||||
echo "✅ 容器启动成功"
|
||||
else
|
||||
echo "❌ 容器启动失败"
|
||||
docker logs ${{ env.CONTAINER_NAME }}
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤6: TG通知 (成功)
|
||||
- name: 📱 发送成功通知到Telegram
|
||||
if: success()
|
||||
uses: appleboy/telegram-action@master
|
||||
with:
|
||||
token: ${{ env.TG_BOT_TOKEN }}
|
||||
to: ${{ env.TG_CHAT_ID }}
|
||||
message: |
|
||||
✅ 部署成功!
|
||||
|
||||
📦 项目: ${{ github.repository }}
|
||||
🌿 分支: ${{ github.ref_name }}
|
||||
📝 提交: ${{ github.sha }}
|
||||
👤 提交者: ${{ github.actor }}
|
||||
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
🚀 服务已成功部署到生产环境
|
||||
parse_mode: Markdown
|
||||
|
||||
# 步骤5: TG通知 (失败)
|
||||
- name: 📱 发送失败通知到Telegram
|
||||
if: failure()
|
||||
uses: appleboy/telegram-action@master
|
||||
with:
|
||||
token: ${{ env.TG_BOT_TOKEN }}
|
||||
to: ${{ env.TG_CHAT_ID }}
|
||||
message: |
|
||||
❌ 部署失败!
|
||||
|
||||
📦 项目: ${{ github.repository }}
|
||||
🌿 分支: ${{ github.ref_name }}
|
||||
📝 提交: ${{ github.sha }}
|
||||
👤 提交者: ${{ github.actor }}
|
||||
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
⚠️ 请检查构建日志获取详细信息
|
||||
parse_mode: Markdown
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
IMAGE_NAME: ppanel-server
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract version from git tag
|
||||
id: version
|
||||
run: echo "VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_ENV
|
||||
|
||||
- name: Get short SHA
|
||||
id: sha
|
||||
run: echo "GIT_SHA=${GITHUB_SHA::8}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set BUILD_TIME env
|
||||
run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV}
|
||||
|
||||
|
||||
- name: Build and push Docker image for main release
|
||||
if: "!contains(github.ref_name, 'beta')"
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
VERSION=${{ env.VERSION }}
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }}
|
||||
|
||||
- name: Build and push Docker image for beta release
|
||||
if: contains(github.ref_name, 'beta')
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
VERSION=${{ env.VERSION }}
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:beta
|
||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }}
|
||||
|
||||
release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-docker
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Install GoReleaser
|
||||
run: |
|
||||
go install github.com/goreleaser/goreleaser/v2@latest
|
||||
|
||||
- name: Run GoReleaser
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
goreleaser check
|
||||
goreleaser release --clean
|
||||
|
||||
releases-matrix:
|
||||
name: Release ppanel-server binary
|
||||
runs-on: ubuntu-latest
|
||||
needs: release-notes # wait for release-notes job to finish
|
||||
strategy:
|
||||
matrix:
|
||||
# build and publish in parallel: linux/386, linux/amd64, linux/arm64,
|
||||
# windows/386, windows/amd64, windows/arm64, darwin/amd64, darwin/arm64
|
||||
goos: [ linux, windows, darwin ]
|
||||
goarch: [ '386', amd64, arm64 ]
|
||||
exclude:
|
||||
- goarch: '386'
|
||||
goos: darwin
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Extract version from git tag
|
||||
id: version
|
||||
run: echo "VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_ENV
|
||||
|
||||
- name: Set BUILD_TIME env
|
||||
run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: wangyoucao577/go-release-action@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
asset_name: "ppanel-server-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||
goversion: "https://dl.google.com/go/go1.23.3.linux-amd64.tar.gz"
|
||||
project_path: "."
|
||||
binary_name: "ppanel-server"
|
||||
extra_files: LICENSE etc
|
||||
ldflags: -X "github.com/perfect-panel/server/pkg/constant.Version=${{env.VERSION}}" -X "github.com/perfect-panel/server/pkg/constant.BuildTime=${{env.BUILD_TIME}}"
|
||||
@@ -3,8 +3,10 @@
|
||||
*-dev.yaml
|
||||
*.local.yaml
|
||||
/test/
|
||||
*.log
|
||||
.DS_Store
|
||||
*_test_config.go
|
||||
*.log*
|
||||
/build/
|
||||
*.p8
|
||||
*.crt
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="go build github.com/perfect-panel/server" type="GoApplicationRunConfiguration" factoryName="Go Application" nameIsGenerated="true">
|
||||
<module name="server" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="run --config etc/ppanel-dev.yaml" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/perfect-panel/server" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/ppanel.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
+2
-4
@@ -24,13 +24,12 @@ RUN BUILD_TIME=$(date -u +"%Y-%m-%d %H:%M:%S") && \
|
||||
go build -ldflags="-s -w -X 'github.com/perfect-panel/server/pkg/constant.Version=${VERSION}' -X 'github.com/perfect-panel/server/pkg/constant.BuildTime=${BUILD_TIME}'" -o /app/ppanel ppanel.go
|
||||
|
||||
# Final minimal image
|
||||
FROM alpine:latest
|
||||
FROM scratch
|
||||
|
||||
# Copy CA certificates and timezone data
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
|
||||
|
||||
# Set Shanghai timezone
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# Set working directory and copy binary
|
||||
@@ -38,7 +37,6 @@ WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/ppanel /app/ppanel
|
||||
COPY --from=builder /build/etc /app/etc
|
||||
COPY --from=builder /build/logs /app/logs
|
||||
|
||||
# Expose the port (optional)
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -1,73 +1,84 @@
|
||||
# Custom configuration | 独立配置
|
||||
# Service name | 项目名称
|
||||
SERVICE=ppanel
|
||||
# Service name in specific style | 项目经过style格式化的名称
|
||||
SERVICE_STYLE=ppanel
|
||||
|
||||
# Service name in lowercase | 项目名称全小写格式
|
||||
SERVICE_LOWER=ppanel
|
||||
# Service name in snake format | 项目名称下划线格式
|
||||
SERVICE_SNAKE=ppanel
|
||||
# Service name in snake format | 项目名称短杠格式
|
||||
SERVICE_DASH=ppanel
|
||||
|
||||
# The project version, if you don't use git, you should set it manually | 项目版本,如果不使用git请手动设置
|
||||
VERSION=$(shell git describe --tags --always)
|
||||
# Build time | 构建时间
|
||||
BUILDTIME=$(shell date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# The project file name style | 项目文件命名风格
|
||||
PROJECT_STYLE=go_zero
|
||||
|
||||
# Whether to use i18n | 是否启用 i18n
|
||||
PROJECT_I18N=true
|
||||
|
||||
# The suffix after build or compile | 构建后缀
|
||||
PROJECT_BUILD_SUFFIX=api
|
||||
|
||||
|
||||
# The arch of the build | 构建的架构
|
||||
GOARCH=amd64
|
||||
|
||||
# The repository of docker | Docker 仓库地址
|
||||
DOCKER_REPO=docker.i.on ticcheck
|
||||
|
||||
# ---- You may not need to modify the codes below | 下面的代码大概率不需要更改 ----
|
||||
|
||||
GO ?= go
|
||||
GOFMT ?= gofmt "-s"
|
||||
GOFILES := $(shell find . -name "*.go")
|
||||
LDFLAGS := -s -w
|
||||
|
||||
# Build command with version injection | 带版本信息注入的构建命令
|
||||
NAME="ppanel-server"
|
||||
BINDIR=bin
|
||||
VERSION=$(shell git describe --tags || echo "unknown version")
|
||||
BUILDTIME=$(shell date -u)
|
||||
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/perfect-panel/server/pkg/constant.Version=$(VERSION)" \
|
||||
-X "github.com/perfect-panel/server/pkg/constant.BuildTime=$(BUILDTIME)" \
|
||||
-w -s -buildid='
|
||||
-X "github.com/perfect-panel/server/pkg/constant.BuildTime=$(BUILDTIME)" \
|
||||
-w -s -buildid='
|
||||
|
||||
.PHONY: docker
|
||||
docker: # Build the docker image | 构建 docker 镜像
|
||||
docker build -f Dockerfile -t ${API_IMAGE_NAME}:${VERSION} .
|
||||
@echo "Build docker successfully"
|
||||
PLATFORM_LIST = \
|
||||
darwin-amd64 \
|
||||
darwin-amd64-v3 \
|
||||
darwin-arm64 \
|
||||
linux-386 \
|
||||
linux-amd64 \
|
||||
linux-amd64-v3 \
|
||||
linux-armv5 \
|
||||
linux-armv6 \
|
||||
linux-armv7 \
|
||||
linux-arm64 \
|
||||
|
||||
.PHONY: publish-docker
|
||||
publish-docker: # Publish docker image | 发布 docker 镜像
|
||||
docker tag ${API_IMAGE_NAME}:${VERSION} ${API_IMAGE_NAME}:latest
|
||||
docker push ${API_IMAGE_NAME}:latest
|
||||
@echo "Publish docker successfully"
|
||||
WINDOWS_ARCH_LIST = \
|
||||
windows-386 \
|
||||
windows-amd64 \
|
||||
windows-amd64-v3 \
|
||||
windows-arm64 \
|
||||
windows-armv7
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run: # Publish docker image | 发布 docker 镜像
|
||||
docker rm -f ${API_CONTAINER_NAME}
|
||||
@echo "pwd: $(pwd)"
|
||||
docker compose -p ${DOCKER_PROJECT_NAME} -f ${DOCKER_COMPOSE_FILE} up -d
|
||||
@echo "docker run successfully"
|
||||
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
||||
|
||||
darwin-amd64:
|
||||
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-amd64-v3:
|
||||
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-arm64:
|
||||
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-386:
|
||||
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64:
|
||||
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64-v3:
|
||||
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-armv5:
|
||||
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-armv6:
|
||||
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-armv7:
|
||||
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-arm64:
|
||||
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
windows-386:
|
||||
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-amd64:
|
||||
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-amd64-v3:
|
||||
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-arm64:
|
||||
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-armv7:
|
||||
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
|
||||
.PHONY: build-linux
|
||||
build-linux: # Build project for Linux | 构建Linux下的可执行文件
|
||||
GOOS=linux GOARCH=$(GOARCH) $(GOBUILD) -o /app/ppanel $(SERVICE_STYLE).go
|
||||
@echo "Build project for Linux successfully"
|
||||
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
||||
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
|
||||
|
||||
.PHONY: help
|
||||
help: # Show help | 显示帮助
|
||||
@grep kfile | sort | while read-r l; do printf "\033[1;32m$$(echo $$l | cut -f 1 -d':')\033[00m:$$(echo $$l | cut -f 2- -d'#')\n"; done
|
||||
$(gz_releases): %.gz : %
|
||||
chmod +x $(BINDIR)/$(NAME)-$(basename $@)
|
||||
gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@)
|
||||
|
||||
$(zip_releases): %.zip : %
|
||||
zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe
|
||||
|
||||
@@ -113,9 +113,9 @@ proxy services. Built with Go, it emphasizes performance, security, and scalabil
|
||||
```
|
||||
|
||||
4. **Pull from Docker Hub** (after CI/CD publishes):
|
||||
```bashw
|
||||
docker pull yourusername/ppanel-server:latest
|
||||
docker run --rm -p 8080:8080 yourusername/ppanel-server:latest
|
||||
```bash
|
||||
docker pull ppanel/ppanel-server:latest
|
||||
docker run --rm -p 8080:8080 ppanel/ppanel-server:latest
|
||||
```
|
||||
|
||||
## 📖 API Documentation
|
||||
|
||||
+47
-29
@@ -102,35 +102,53 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) {
|
||||
for _, protocol := range protocols {
|
||||
if protocol.Type == item.Protocol {
|
||||
proxies = append(proxies, Proxy{
|
||||
Sort: item.Sort,
|
||||
Name: item.Name,
|
||||
Server: item.Address,
|
||||
Port: item.Port,
|
||||
Type: item.Protocol,
|
||||
Tags: strings.Split(item.Tags, ","),
|
||||
Security: protocol.Security,
|
||||
SNI: protocol.SNI,
|
||||
AllowInsecure: protocol.AllowInsecure,
|
||||
Fingerprint: protocol.Fingerprint,
|
||||
RealityServerAddr: protocol.RealityServerAddr,
|
||||
RealityServerPort: protocol.RealityServerPort,
|
||||
RealityPrivateKey: protocol.RealityPrivateKey,
|
||||
RealityPublicKey: protocol.RealityPublicKey,
|
||||
RealityShortId: protocol.RealityShortId,
|
||||
Transport: protocol.Transport,
|
||||
Host: protocol.Host,
|
||||
Path: protocol.Path,
|
||||
ServiceName: protocol.ServiceName,
|
||||
Method: protocol.Cipher,
|
||||
ServerKey: protocol.ServerKey,
|
||||
Flow: protocol.Flow,
|
||||
HopPorts: protocol.HopPorts,
|
||||
HopInterval: protocol.HopInterval,
|
||||
ObfsPassword: protocol.ObfsPassword,
|
||||
DisableSNI: protocol.DisableSNI,
|
||||
ReduceRtt: protocol.ReduceRtt,
|
||||
UDPRelayMode: protocol.UDPRelayMode,
|
||||
CongestionController: protocol.CongestionController,
|
||||
Sort: item.Sort,
|
||||
Name: item.Name,
|
||||
Server: item.Address,
|
||||
Port: item.Port,
|
||||
Type: item.Protocol,
|
||||
Tags: strings.Split(item.Tags, ","),
|
||||
Security: protocol.Security,
|
||||
SNI: protocol.SNI,
|
||||
AllowInsecure: protocol.AllowInsecure,
|
||||
Fingerprint: protocol.Fingerprint,
|
||||
RealityServerAddr: protocol.RealityServerAddr,
|
||||
RealityServerPort: protocol.RealityServerPort,
|
||||
RealityPrivateKey: protocol.RealityPrivateKey,
|
||||
RealityPublicKey: protocol.RealityPublicKey,
|
||||
RealityShortId: protocol.RealityShortId,
|
||||
Transport: protocol.Transport,
|
||||
Host: protocol.Host,
|
||||
Path: protocol.Path,
|
||||
ServiceName: protocol.ServiceName,
|
||||
Method: protocol.Cipher,
|
||||
ServerKey: protocol.ServerKey,
|
||||
Flow: protocol.Flow,
|
||||
HopPorts: protocol.HopPorts,
|
||||
HopInterval: protocol.HopInterval,
|
||||
ObfsPassword: protocol.ObfsPassword,
|
||||
UpMbps: protocol.UpMbps,
|
||||
DownMbps: protocol.DownMbps,
|
||||
DisableSNI: protocol.DisableSNI,
|
||||
ReduceRtt: protocol.ReduceRtt,
|
||||
UDPRelayMode: protocol.UDPRelayMode,
|
||||
CongestionController: protocol.CongestionController,
|
||||
PaddingScheme: protocol.PaddingScheme,
|
||||
Multiplex: protocol.Multiplex,
|
||||
XhttpMode: protocol.XhttpMode,
|
||||
XhttpExtra: protocol.XhttpExtra,
|
||||
Encryption: protocol.Encryption,
|
||||
EncryptionMode: protocol.EncryptionMode,
|
||||
EncryptionRtt: protocol.EncryptionRtt,
|
||||
EncryptionTicket: protocol.EncryptionTicket,
|
||||
EncryptionServerPadding: protocol.EncryptionServerPadding,
|
||||
EncryptionPrivateKey: protocol.EncryptionPrivateKey,
|
||||
EncryptionClientPadding: protocol.EncryptionClientPadding,
|
||||
EncryptionPassword: protocol.EncryptionPassword,
|
||||
Ratio: protocol.Ratio,
|
||||
CertMode: protocol.CertMode,
|
||||
CertDNSProvider: protocol.CertDNSProvider,
|
||||
CertDNSEnv: protocol.CertDNSEnv,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@ type (
|
||||
BatchDeleteSubscribeRequest {
|
||||
Ids []int64 `json:"ids" validate:"required"`
|
||||
}
|
||||
ResetAllSubscribeTokenResponse {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
@@ -157,5 +160,9 @@ service ppanel {
|
||||
@doc "Subscribe sort"
|
||||
@handler SubscribeSort
|
||||
post /sort (SubscribeSortRequest)
|
||||
|
||||
@doc "Reset all subscribe tokens"
|
||||
@handler ResetAllSubscribeToken
|
||||
post /reset_all_token returns (ResetAllSubscribeTokenResponse)
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -19,8 +19,13 @@ type (
|
||||
Periods []TimePeriod `json:"periods"`
|
||||
}
|
||||
PreViewNodeMultiplierResponse {
|
||||
CurrentTime string `json:"current_time"`
|
||||
Ratio float32 `json:"ratio"`
|
||||
CurrentTime string `json:"current_time"`
|
||||
Ratio float32 `json:"ratio"`
|
||||
}
|
||||
ModuleConfig {
|
||||
Secret string `json:"secret"` // 通讯密钥
|
||||
ServiceName string `json:"service_name"` // 服务名称
|
||||
ServiceVersion string `json:"service_version"` // 服务版本
|
||||
}
|
||||
)
|
||||
|
||||
@@ -125,5 +130,9 @@ service ppanel {
|
||||
@doc "PreView Node Multiplier"
|
||||
@handler PreViewNodeMultiplier
|
||||
get /node_multiplier/preview returns (PreViewNodeMultiplierResponse)
|
||||
|
||||
@doc "Get Module Config"
|
||||
@handler GetModuleConfig
|
||||
get /module returns (ModuleConfig)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,11 @@ type (
|
||||
Status *uint8 `json:"status" validate:"required"`
|
||||
}
|
||||
GetTicketListRequest {
|
||||
Page int64 `form:"page"`
|
||||
Size int64 `form:"size"`
|
||||
UserId int64 `form:"user_id,omitempty"`
|
||||
Status *uint8 `form:"status,omitempty"`
|
||||
IssueType *uint8 `form:"issue_type,omitempty"`
|
||||
Search string `form:"search,omitempty"`
|
||||
Page int64 `form:"page"`
|
||||
Size int64 `form:"size"`
|
||||
UserId int64 `form:"user_id,omitempty"`
|
||||
Status *uint8 `form:"status,omitempty"`
|
||||
Search string `form:"search,omitempty"`
|
||||
}
|
||||
GetTicketListResponse {
|
||||
Total int64 `json:"total"`
|
||||
|
||||
@@ -17,6 +17,14 @@ type (
|
||||
VersionResponse {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
QueryIPLocationRequest {
|
||||
IP string `form:"ip" validate:"required"`
|
||||
}
|
||||
QueryIPLocationResponse {
|
||||
Country string `json:"country"`
|
||||
Region string `json:"region,omitempty"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
@@ -36,5 +44,9 @@ service ppanel {
|
||||
@doc "Get Version"
|
||||
@handler GetVersion
|
||||
get /version returns (VersionResponse)
|
||||
|
||||
@doc "Query IP Location"
|
||||
@handler QueryIPLocation
|
||||
get /ip/location (QueryIPLocationRequest) returns (QueryIPLocationResponse)
|
||||
}
|
||||
|
||||
|
||||
+43
-20
@@ -11,11 +11,13 @@ info (
|
||||
type (
|
||||
// User login request
|
||||
UserLoginRequest {
|
||||
Email string `json:"email" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
}
|
||||
// Check user is exist request
|
||||
CheckUserRequest {
|
||||
@@ -27,22 +29,26 @@ type (
|
||||
}
|
||||
// User login response
|
||||
UserRegisterRequest {
|
||||
Email string `json:"email" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Invite string `json:"invite,optional"`
|
||||
Code string `json:"code,optional"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Invite string `json:"invite,optional"`
|
||||
Code string `json:"code,optional"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
}
|
||||
// User login response
|
||||
ResetPasswordRequest {
|
||||
Email string `json:"email" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Code string `json:"code,optional"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Code string `json:"code,optional"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
}
|
||||
LoginResponse {
|
||||
Token string `json:"token"`
|
||||
@@ -60,12 +66,14 @@ type (
|
||||
}
|
||||
// login request
|
||||
TelephoneLoginRequest {
|
||||
Identifier string `json:"identifier"`
|
||||
Telephone string `json:"telephone" validate:"required"`
|
||||
TelephoneCode string `json:"telephone_code"`
|
||||
TelephoneAreaCode string `json:"telephone_area_code" validate:"required"`
|
||||
Password string `json:"password"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
}
|
||||
// Check user is exist request
|
||||
@@ -79,6 +87,7 @@ type (
|
||||
}
|
||||
// User login response
|
||||
TelephoneRegisterRequest {
|
||||
Identifier string `json:"identifier"`
|
||||
Telephone string `json:"telephone" validate:"required"`
|
||||
TelephoneAreaCode string `json:"telephone_area_code" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
@@ -86,16 +95,19 @@ type (
|
||||
Code string `json:"code,optional"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type,optional"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
}
|
||||
// User login response
|
||||
TelephoneResetPasswordRequest {
|
||||
Identifier string `json:"identifier"`
|
||||
Telephone string `json:"telephone" validate:"required"`
|
||||
TelephoneAreaCode string `json:"telephone_area_code" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Code string `json:"code,optional"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type,optional"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
}
|
||||
AppleLoginCallbackRequest {
|
||||
@@ -107,11 +119,18 @@ type (
|
||||
Code string `form:"code"`
|
||||
State string `form:"state"`
|
||||
}
|
||||
DeviceLoginRequest {
|
||||
Identifier string `json:"identifier" validate:"required"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `json:"user_agent" validate:"required"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
prefix: v1/auth
|
||||
group: auth
|
||||
prefix: v1/auth
|
||||
group: auth
|
||||
middleware: DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "User login"
|
||||
@@ -145,6 +164,10 @@ service ppanel {
|
||||
@doc "Reset password"
|
||||
@handler TelephoneResetPassword
|
||||
post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse)
|
||||
|
||||
@doc "Device Login"
|
||||
@handler DeviceLogin
|
||||
post /login/device (DeviceLoginRequest) returns (LoginResponse)
|
||||
}
|
||||
|
||||
@server (
|
||||
|
||||
+12
-2
@@ -87,11 +87,17 @@ type (
|
||||
Total int64 `json:"total"`
|
||||
List []SubscribeClient `json:"list"`
|
||||
}
|
||||
HeartbeatResponse {
|
||||
Status bool `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
prefix: v1/common
|
||||
group: common
|
||||
prefix: v1/common
|
||||
group: common
|
||||
middleware: DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "Get global config"
|
||||
@@ -129,5 +135,9 @@ service ppanel {
|
||||
@doc "Get Client"
|
||||
@handler GetClient
|
||||
get /client returns (GetSubscribeClientResponse)
|
||||
|
||||
@doc "Heartbeat"
|
||||
@handler Heartbeat
|
||||
get /heartbeat returns (HeartbeatResponse)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import "../types.api"
|
||||
@server (
|
||||
prefix: v1/public/announcement
|
||||
group: public/announcement
|
||||
middleware: AuthMiddleware
|
||||
middleware: AuthMiddleware,DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "Query announcement"
|
||||
|
||||
@@ -13,7 +13,7 @@ import "../types.api"
|
||||
@server (
|
||||
prefix: v1/public/document
|
||||
group: public/document
|
||||
middleware: AuthMiddleware
|
||||
middleware: AuthMiddleware,DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "Get document list"
|
||||
|
||||
@@ -13,7 +13,7 @@ import "../types.api"
|
||||
@server (
|
||||
prefix: v1/public/order
|
||||
group: public/order
|
||||
middleware: AuthMiddleware
|
||||
middleware: AuthMiddleware,DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "Pre create order"
|
||||
|
||||
@@ -13,7 +13,7 @@ import "../types.api"
|
||||
@server (
|
||||
prefix: v1/public/payment
|
||||
group: public/payment
|
||||
middleware: AuthMiddleware
|
||||
middleware: AuthMiddleware,DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "Get available payment methods"
|
||||
|
||||
@@ -68,8 +68,9 @@ type (
|
||||
)
|
||||
|
||||
@server (
|
||||
prefix: v1/public/portal
|
||||
group: public/portal
|
||||
prefix: v1/public/portal
|
||||
group: public/portal
|
||||
middleware: DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "Get available payment methods"
|
||||
|
||||
@@ -14,16 +14,54 @@ type (
|
||||
QuerySubscribeListRequest {
|
||||
Language string `form:"language"`
|
||||
}
|
||||
QueryUserSubscribeNodeListResponse {
|
||||
List []UserSubscribeInfo `json:"list"`
|
||||
}
|
||||
UserSubscribeInfo {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
FinishedAt int64 `json:"finished_at"`
|
||||
ResetTime int64 `json:"reset_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
IsTryOut bool `json:"is_try_out"`
|
||||
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
||||
}
|
||||
UserSubscribeNodeInfo {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Uuid string `json:"uuid"`
|
||||
Protocol string `json:"protocol"`
|
||||
Port uint16 `json:"port"`
|
||||
Address string `json:"address"`
|
||||
Tags []string `json:"tags"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
prefix: v1/public/subscribe
|
||||
group: public/subscribe
|
||||
middleware: AuthMiddleware
|
||||
middleware: AuthMiddleware,DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "Get subscribe list"
|
||||
@handler QuerySubscribeList
|
||||
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
|
||||
|
||||
@doc "Get user subscribe node info"
|
||||
@handler QueryUserSubscribeNodeList
|
||||
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,12 @@ type (
|
||||
CreateUserTicketRequest {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
IssueType uint8 `json:"issue_type"`
|
||||
}
|
||||
GetUserTicketListRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Status *uint8 `form:"status,omitempty"`
|
||||
IssueType *uint8 `form:"issue_type,omitempty"`
|
||||
Search string `form:"search,omitempty"`
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Status *uint8 `form:"status,omitempty"`
|
||||
Search string `form:"search,omitempty"`
|
||||
}
|
||||
GetUserTicketDetailRequest {
|
||||
Id int64 `form:"id" validate:"required"`
|
||||
@@ -45,7 +43,7 @@ type (
|
||||
@server (
|
||||
prefix: v1/public/ticket
|
||||
group: public/ticket
|
||||
middleware: AuthMiddleware
|
||||
middleware: AuthMiddleware,DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "Get ticket list"
|
||||
|
||||
+61
-1
@@ -97,12 +97,48 @@ type (
|
||||
Email string `json:"email" validate:"required"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
GetDeviceListResponse {
|
||||
List []UserDevice `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
UnbindDeviceRequest {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
}
|
||||
UpdateUserSubscribeNoteRequest {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
||||
Note string `json:"note" validate:"max=500"`
|
||||
}
|
||||
UpdateUserRulesRequest {
|
||||
Rules []string `json:"rules" validate:"required"`
|
||||
}
|
||||
CommissionWithdrawRequest {
|
||||
Amount int64 `json:"amount"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
WithdrawalLog {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
Amount int64 `json:"amount"`
|
||||
Content string `json:"content"`
|
||||
Status uint8 `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
QueryWithdrawalLogListRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
}
|
||||
QueryWithdrawalLogListResponse {
|
||||
List []WithdrawalLog `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
prefix: v1/public/user
|
||||
group: public/user
|
||||
middleware: AuthMiddleware
|
||||
middleware: AuthMiddleware,DeviceMiddleware
|
||||
)
|
||||
service ppanel {
|
||||
@doc "Query User Info"
|
||||
@@ -192,5 +228,29 @@ service ppanel {
|
||||
@doc "Update Bind Email"
|
||||
@handler UpdateBindEmail
|
||||
put /bind_email (UpdateBindEmailRequest)
|
||||
|
||||
@doc "Get Device List"
|
||||
@handler GetDeviceList
|
||||
get /devices returns (GetDeviceListResponse)
|
||||
|
||||
@doc "Unbind Device"
|
||||
@handler UnbindDevice
|
||||
put /unbind_device (UnbindDeviceRequest)
|
||||
|
||||
@doc "Update User Subscribe Note"
|
||||
@handler UpdateUserSubscribeNote
|
||||
put /subscribe_note (UpdateUserSubscribeNoteRequest)
|
||||
|
||||
@doc "Update User Rules"
|
||||
@handler UpdateUserRules
|
||||
put /rules (UpdateUserRulesRequest)
|
||||
|
||||
@doc "Commission Withdraw"
|
||||
@handler CommissionWithdraw
|
||||
post /commission_withdraw (CommissionWithdrawRequest) returns (WithdrawalLog)
|
||||
|
||||
@doc "Query Withdrawal Log"
|
||||
@handler QueryWithdrawalLog
|
||||
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ type (
|
||||
EnableTradeNotify bool `json:"enable_trade_notify"`
|
||||
AuthMethods []UserAuthMethod `json:"auth_methods"`
|
||||
UserDevices []UserDevice `json:"user_devices"`
|
||||
Rules []string `json:"rules"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
DeletedAt int64 `json:"deleted_at,omitempty"`
|
||||
@@ -115,6 +116,7 @@ type (
|
||||
AuthConfig {
|
||||
Mobile MobileAuthenticateConfig `json:"mobile"`
|
||||
Email EmailAuthticateConfig `json:"email"`
|
||||
Device DeviceAuthticateConfig `json:"device"`
|
||||
Register PubilcRegisterConfig `json:"register"`
|
||||
}
|
||||
PubilcRegisterConfig {
|
||||
@@ -134,6 +136,12 @@ type (
|
||||
EnableDomainSuffix bool `json:"enable_domain_suffix"`
|
||||
DomainSuffixList string `json:"domain_suffix_list"`
|
||||
}
|
||||
DeviceAuthticateConfig {
|
||||
Enable bool `json:"enable"`
|
||||
ShowAds bool `json:"show_ads"`
|
||||
EnableSecurity bool `json:"enable_security"`
|
||||
OnlyRealDevice bool `json:"only_real_device"`
|
||||
}
|
||||
RegisterConfig {
|
||||
StopRegister bool `json:"stop_register"`
|
||||
EnableTrial bool `json:"enable_trial"`
|
||||
@@ -464,6 +472,7 @@ type (
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
Short string `json:"short"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/perfect-panel/server/initialize"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/conf"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrateCmd.Flags().StringVar(&migrateConfigPath, "config", "etc/ppanel.yaml", "ppanel.yaml directory to read from")
|
||||
rootCmd.AddCommand(migrateCmd)
|
||||
}
|
||||
|
||||
var (
|
||||
migrateConfigPath string
|
||||
)
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Run database migrations",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runMigrate()
|
||||
},
|
||||
}
|
||||
|
||||
func runMigrate() {
|
||||
var c config.Config
|
||||
conf.MustLoad(migrateConfigPath, &c)
|
||||
if err := logger.SetUp(c.Logger); err != nil {
|
||||
fmt.Println("Logger setup failed:", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx := svc.NewServiceContext(c)
|
||||
initialize.Migrate(ctx)
|
||||
logger.Info("[Migrate] database migration completed")
|
||||
}
|
||||
+4
-16
@@ -2,11 +2,11 @@ Host: 0.0.0.0
|
||||
Port: 8080
|
||||
Debug: false
|
||||
JwtAuth:
|
||||
AccessSecret: e4bbdb8ca57179374c0e8a7a85038da21341aee4bdff98c0e612751d7c362cac
|
||||
AccessSecret: 1234567890
|
||||
AccessExpire: 604800
|
||||
Logger:
|
||||
ServiceName: PPanel
|
||||
Mode: file
|
||||
Mode: console
|
||||
Encoding: plain
|
||||
TimeFormat: '2025-01-01 00:00:00.000'
|
||||
Path: logs
|
||||
@@ -21,7 +21,7 @@ Logger:
|
||||
Rotation: daily
|
||||
FileTimeFormat: 2025-01-01T00:00:00.000Z00:00
|
||||
MySQL:
|
||||
Addr: ppanel-db:3306
|
||||
Addr: 172.245.180.199:3306
|
||||
Dbname: ppanel
|
||||
Username: ppanel
|
||||
Password: ppanelpassword
|
||||
@@ -35,16 +35,4 @@ Redis:
|
||||
DB: 0
|
||||
Administrator:
|
||||
Password: password
|
||||
Email: admin@ppanel.dev
|
||||
Email:
|
||||
Enable: true
|
||||
Platform: smtp
|
||||
PlatformConfig: '{"host":"smtp.gmail.com","port":465,"user":"devneeds52@gmail.com","pass":"bqxn hurw ckxt ookj","from":"devneeds52@gmail.com","ssl":true}'
|
||||
EnableVerify: true
|
||||
EnableNotify: true
|
||||
EnableDomainSuffix: false
|
||||
DomainSuffixList: ""
|
||||
VerifyEmailTemplate: ""
|
||||
ExpirationEmailTemplate: ""
|
||||
MaintenanceEmailTemplate: ""
|
||||
TrafficExceedEmailTemplate: ""
|
||||
Email: admin@ppanel.dev
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -44,7 +44,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.29.0
|
||||
go.opentelemetry.io/otel/trace v1.29.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/oauth2 v0.25.0
|
||||
golang.org/x/time v0.6.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
@@ -60,6 +60,7 @@ require (
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/goccy/go-json v0.10.4
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
github.com/oschwald/geoip2-golang v1.13.0
|
||||
github.com/spaolacci/murmur3 v1.1.0
|
||||
google.golang.org/grpc v1.64.1
|
||||
google.golang.org/protobuf v1.36.3
|
||||
@@ -117,6 +118,7 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.4.2 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
@@ -138,8 +140,8 @@ require (
|
||||
golang.org/x/arch v0.13.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
|
||||
@@ -285,6 +285,10 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
|
||||
github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY=
|
||||
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -402,8 +406,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
@@ -463,8 +467,8 @@ 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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=
|
||||
@@ -478,8 +482,8 @@ 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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
|
||||
+22
-1
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/perfect-panel/server/internal/report"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"gorm.io/driver/mysql"
|
||||
|
||||
@@ -35,10 +36,30 @@ func Config(path string) (chan bool, *http.Server) {
|
||||
configPath = path
|
||||
// Create a new Gin instance
|
||||
r := gin.Default()
|
||||
// get server port
|
||||
port := 8080
|
||||
host := "127.0.0.1"
|
||||
|
||||
// check gateway mode
|
||||
if report.IsGatewayMode() {
|
||||
// get free port
|
||||
freePort, err := report.ModulePort()
|
||||
if err != nil {
|
||||
logger.Errorf("get module port error: %s", err.Error())
|
||||
panic(err)
|
||||
}
|
||||
port = freePort
|
||||
// register module
|
||||
err = report.RegisterModule(port)
|
||||
if err != nil {
|
||||
logger.Errorf("register module error: %s", err.Error())
|
||||
panic(err)
|
||||
}
|
||||
logger.Infof("module registered on port %d", port)
|
||||
}
|
||||
// Create a new HTTP server
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
Addr: fmt.Sprintf("%s:%d", host, port),
|
||||
Handler: r,
|
||||
}
|
||||
// Load templates
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/model/auth"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
)
|
||||
|
||||
func Device(ctx *svc.ServiceContext) {
|
||||
logger.Debug("device config initialization")
|
||||
method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "device")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var cfg config.DeviceConfig
|
||||
var deviceConfig auth.DeviceConfig
|
||||
deviceConfig.Unmarshal(method.Config)
|
||||
tool.DeepCopy(&cfg, deviceConfig)
|
||||
cfg.Enable = *method.Enabled
|
||||
ctx.Config.Device = cfg
|
||||
}
|
||||
@@ -9,6 +9,7 @@ func StartInitSystemConfig(svc *svc.ServiceContext) {
|
||||
Site(svc)
|
||||
Node(svc)
|
||||
Email(svc)
|
||||
Device(svc)
|
||||
Invite(svc)
|
||||
Verify(svc)
|
||||
Subscribe(svc)
|
||||
|
||||
@@ -379,7 +379,6 @@ CREATE TABLE IF NOT EXISTS `user`
|
||||
`referer_id` bigint DEFAULT NULL COMMENT 'Referrer ID',
|
||||
`commission` bigint DEFAULT '0' COMMENT 'Commission',
|
||||
`gift_amount` bigint DEFAULT '0' COMMENT 'User Gift Amount',
|
||||
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'User Remark',
|
||||
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Is Account Enabled',
|
||||
`is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Admin',
|
||||
`valid_email` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Email Verified',
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- 只有当 ads 表中不存在 description 字段时才添加
|
||||
SET
|
||||
@col_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'ads'
|
||||
AND COLUMN_NAME = 'description'
|
||||
);
|
||||
|
||||
SET
|
||||
@query := IF(
|
||||
@col_exists = 0,
|
||||
'ALTER TABLE `ads` ADD COLUMN `description` VARCHAR(255) DEFAULT '''' COMMENT ''Description'';',
|
||||
'SELECT "Column `description` already exists"'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @query;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `user`
|
||||
DROP COLUMN `algo`,
|
||||
DROP COLUMN `salt`;
|
||||
@@ -0,0 +1,35 @@
|
||||
-- 添加 algo 列(如果不存在)
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'user';
|
||||
SET @colname = 'algo';
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `user` ADD COLUMN `algo` VARCHAR(20) NOT NULL DEFAULT ''default'' COMMENT ''Encryption Algorithm'' AFTER `password`;',
|
||||
'SELECT "Column `algo` already exists";'
|
||||
)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname
|
||||
AND TABLE_NAME = @tablename
|
||||
AND COLUMN_NAME = @colname
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 添加 salt 列(如果不存在)
|
||||
SET @colname = 'salt';
|
||||
SET @sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `user` ADD COLUMN `salt` VARCHAR(20) NOT NULL DEFAULT ''default'' COMMENT ''Password Salt'' AFTER `algo`;',
|
||||
'SELECT "Column `salt` already exists";'
|
||||
)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname
|
||||
AND TABLE_NAME = @tablename
|
||||
AND COLUMN_NAME = @colname
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -0,0 +1,7 @@
|
||||
INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||
SELECT 'site', 'CustomData', '{
|
||||
"kr_website_id": ""
|
||||
}', 'string', 'Custom Data', '2025-04-22 14:25:16.637', '2025-10-14 15:47:19.187'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData'
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||
SELECT 'site', 'CustomData', '{
|
||||
"kr_website_id": ""
|
||||
}', 'string', 'Custom Data', '2025-04-22 14:25:16.637', '2025-10-14 15:47:19.187'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData'
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE traffic_log DROP INDEX idx_timestamp;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `user_subscribe`
|
||||
DROP COLUMN `note`;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `user_subscribe`
|
||||
ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT ''
|
||||
COMMENT 'User note for subscription'
|
||||
AFTER `status`;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `user`
|
||||
DROP COLUMN IF EXISTS `rules`;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `rules` TEXT NULL
|
||||
COMMENT 'User rules for subscription'
|
||||
AFTER `created_at`;
|
||||
@@ -0,0 +1,5 @@
|
||||
DROP TABLE IF EXISTS `withdrawals`;
|
||||
|
||||
DELETE FROM `system`
|
||||
WHERE `category` = 'invite'
|
||||
AND `key` = 'WithdrawalMethod';
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS `withdrawals` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
|
||||
`user_id` BIGINT NOT NULL COMMENT 'User ID',
|
||||
`amount` BIGINT NOT NULL COMMENT 'Withdrawal Amount',
|
||||
`content` TEXT COMMENT 'Withdrawal Content',
|
||||
`status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Withdrawal Status',
|
||||
`reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Rejection Reason',
|
||||
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
|
||||
`updated_at` DATETIME NOT NULL COMMENT 'Update Time',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||
VALUES
|
||||
('invite', 'WithdrawalMethod', '', 'string', 'withdrawal method', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637');
|
||||
@@ -2,6 +2,7 @@ package initialize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"gorm.io/gorm"
|
||||
@@ -16,6 +17,7 @@ func Migrate(ctx *svc.ServiceContext) {
|
||||
mc := orm.Mysql{
|
||||
Config: ctx.Config.MySQL,
|
||||
}
|
||||
now := time.Now()
|
||||
if err := migrate.Migrate(mc.Dsn()).Up(); err != nil {
|
||||
if errors.Is(err, migrate.NoChange) {
|
||||
logger.Info("[Migrate] database not change")
|
||||
@@ -23,6 +25,8 @@ func Migrate(ctx *svc.ServiceContext) {
|
||||
}
|
||||
logger.Errorf("[Migrate] Up error: %v", err.Error())
|
||||
panic(err)
|
||||
} else {
|
||||
logger.Info("[Migrate] Database change, took " + time.Since(now).String())
|
||||
}
|
||||
// if not found admin user
|
||||
err := ctx.DB.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/orm"
|
||||
)
|
||||
@@ -20,6 +21,7 @@ type Config struct {
|
||||
Node NodeConfig `yaml:"Node"`
|
||||
Mobile MobileConfig `yaml:"Mobile"`
|
||||
Email EmailConfig `yaml:"Email"`
|
||||
Device DeviceConfig `yaml:"device"`
|
||||
Verify Verify `yaml:"Verify"`
|
||||
VerifyCode VerifyCode `yaml:"VerifyCode"`
|
||||
Register RegisterConfig `yaml:"Register"`
|
||||
@@ -95,6 +97,14 @@ type MobileConfig struct {
|
||||
Whitelist []string `yaml:"whitelist"`
|
||||
}
|
||||
|
||||
type DeviceConfig struct {
|
||||
Enable bool `yaml:"enable" default:"true"`
|
||||
ShowAds bool `yaml:"show_ads"`
|
||||
EnableSecurity bool `yaml:"enable_security"`
|
||||
OnlyRealDevice bool `yaml:"only_real_device"`
|
||||
SecuritySecret string `yaml:"security_secret"`
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Host string `yaml:"Host" default:""`
|
||||
SiteName string `yaml:"SiteName" default:""`
|
||||
@@ -190,10 +200,12 @@ type File struct {
|
||||
}
|
||||
|
||||
type InviteConfig struct {
|
||||
ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
|
||||
FirstPurchasePercentage int64 `yaml:"FirstPurchasePercentage" default:"20"`
|
||||
ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
|
||||
FirstPurchasePercentage int64 `yaml:"FirstPurchasePercentage" default:"20"`
|
||||
FirstYearlyPurchasePercentage int64 `yaml:"FirstYearlyPurchasePercentage" default:"25"`
|
||||
NonFirstPurchasePercentage int64 `yaml:"NonFirstPurchasePercentage" default:"10"`
|
||||
NonFirstPurchasePercentage int64 `yaml:"NonFirstPurchasePercentage" default:"10"`
|
||||
ReferralPercentage uint8 `yaml:"ReferralPercentage" default:"10"`
|
||||
OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"`
|
||||
}
|
||||
|
||||
type Telegram struct {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package subscribe
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/subscribe"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Reset all subscribe tokens
|
||||
func ResetAllSubscribeTokenHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
l := subscribe.NewResetAllSubscribeTokenLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.ResetAllSubscribeToken()
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/system"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// GetModuleConfigHandler Get Module Config
|
||||
func GetModuleConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
l := system.NewGetModuleConfigLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetModuleConfig()
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/tool"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// QueryIPLocationHandler Query IP Location
|
||||
func QueryIPLocationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.QueryIPLocationRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := tool.NewQueryIPLocationLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.QueryIPLocation(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/auth"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Device Login
|
||||
func DeviceLoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.DeviceLoginRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := auth.NewDeviceLoginLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.DeviceLogin(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
// Get Client
|
||||
func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
l := common.NewGetClientLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetClient()
|
||||
result.HttpResult(c, resp, err)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/common"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Heartbeat
|
||||
func HeartbeatHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
l := common.NewHeartbeatLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.Heartbeat()
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package subscribe
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/subscribe"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Get user subscribe node info
|
||||
func QueryUserSubscribeNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
l := subscribe.NewQueryUserSubscribeNodeListLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.QueryUserSubscribeNodeList()
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Commission Withdraw
|
||||
func CommissionWithdrawHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.CommissionWithdrawRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewCommissionWithdrawLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.CommissionWithdraw(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Get Device List
|
||||
func GetDeviceListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
l := user.NewGetDeviceListLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetDeviceList()
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
@@ -12,15 +11,8 @@ import (
|
||||
func QueryUserBalanceLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
var req types.QueryUserBalanceLogListRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
l := user.NewQueryUserBalanceLogLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.QueryUserBalanceLog(&req)
|
||||
resp, err := l.QueryUserBalanceLog()
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Query Withdrawal Log
|
||||
func QueryWithdrawalLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.QueryWithdrawalLogListRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewQueryWithdrawalLogLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.QueryWithdrawalLog(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Unbind Device
|
||||
func UnbindDeviceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.UnbindDeviceRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewUnbindDeviceLogic(c.Request.Context(), svcCtx)
|
||||
err := l.UnbindDevice(&req)
|
||||
result.HttpResult(c, nil, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Update User Rules
|
||||
func UpdateUserRulesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.UpdateUserRulesRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewUpdateUserRulesLogic(c.Request.Context(), svcCtx)
|
||||
err := l.UpdateUserRules(&req)
|
||||
result.HttpResult(c, nil, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Update User Subscribe Note
|
||||
func UpdateUserSubscribeNoteHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.UpdateUserSubscribeNoteRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewUpdateUserSubscribeNoteLogic(c.Request.Context(), svcCtx)
|
||||
err := l.UpdateUserSubscribeNote(&req)
|
||||
result.HttpResult(c, nil, err)
|
||||
}
|
||||
}
|
||||
@@ -385,6 +385,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// Get subscribe list
|
||||
adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx))
|
||||
|
||||
// Reset all subscribe tokens
|
||||
adminSubscribeGroupRouter.POST("/reset_all_token", adminSubscribe.ResetAllSubscribeTokenHandler(serverCtx))
|
||||
|
||||
// Subscribe sort
|
||||
adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx))
|
||||
}
|
||||
@@ -408,6 +411,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// Update invite config
|
||||
adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx))
|
||||
|
||||
// Get Module Config
|
||||
adminSystemGroupRouter.GET("/module", adminSystem.GetModuleConfigHandler(serverCtx))
|
||||
|
||||
// Get node config
|
||||
adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx))
|
||||
|
||||
@@ -487,6 +493,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Query IP Location
|
||||
adminToolGroupRouter.GET("/ip/location", adminTool.QueryIPLocationHandler(serverCtx))
|
||||
|
||||
// Get System Log
|
||||
adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx))
|
||||
|
||||
@@ -578,6 +587,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
}
|
||||
|
||||
authGroupRouter := router.Group("/v1/auth")
|
||||
authGroupRouter.Use(middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Check user is exist
|
||||
@@ -589,6 +599,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// User login
|
||||
authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx))
|
||||
|
||||
// Device Login
|
||||
authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx))
|
||||
|
||||
// User Telephone login
|
||||
authGroupRouter.POST("/login/telephone", auth.TelephoneLoginHandler(serverCtx))
|
||||
|
||||
@@ -619,6 +632,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
}
|
||||
|
||||
commonGroupRouter := router.Group("/v1/common")
|
||||
commonGroupRouter.Use(middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Get Ads
|
||||
@@ -630,6 +644,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// Get Client
|
||||
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
|
||||
|
||||
// Heartbeat
|
||||
commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx))
|
||||
|
||||
// Get verification code
|
||||
commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx))
|
||||
|
||||
@@ -650,7 +667,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
}
|
||||
|
||||
publicAnnouncementGroupRouter := router.Group("/v1/public/announcement")
|
||||
publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||
publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Query announcement
|
||||
@@ -658,7 +675,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
}
|
||||
|
||||
publicDocumentGroupRouter := router.Group("/v1/public/document")
|
||||
publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||
publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Get document detail
|
||||
@@ -669,7 +686,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
}
|
||||
|
||||
publicOrderGroupRouter := router.Group("/v1/public/order")
|
||||
publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||
publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Close order
|
||||
@@ -698,7 +715,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
}
|
||||
|
||||
publicPaymentGroupRouter := router.Group("/v1/public/payment")
|
||||
publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||
publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Get available payment methods
|
||||
@@ -706,6 +723,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
}
|
||||
|
||||
publicPortalGroupRouter := router.Group("/v1/public/portal")
|
||||
publicPortalGroupRouter.Use(middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Purchase Checkout
|
||||
@@ -728,15 +746,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
}
|
||||
|
||||
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
|
||||
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Get subscribe list
|
||||
publicSubscribeGroupRouter.GET("/list", publicSubscribe.QuerySubscribeListHandler(serverCtx))
|
||||
|
||||
// Get user subscribe node info
|
||||
publicSubscribeGroupRouter.GET("/node/list", publicSubscribe.QueryUserSubscribeNodeListHandler(serverCtx))
|
||||
}
|
||||
|
||||
publicTicketGroupRouter := router.Group("/v1/public/ticket")
|
||||
publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||
publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Update ticket status
|
||||
@@ -756,7 +777,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
}
|
||||
|
||||
publicUserGroupRouter := router.Group("/v1/public/user")
|
||||
publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||
publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Query User Affiliate Count
|
||||
@@ -786,6 +807,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// Query User Commission Log
|
||||
publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx))
|
||||
|
||||
// Commission Withdraw
|
||||
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
|
||||
|
||||
// Get Device List
|
||||
publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx))
|
||||
|
||||
// Query User Info
|
||||
publicUserGroupRouter.GET("/info", publicUser.QueryUserInfoHandler(serverCtx))
|
||||
|
||||
@@ -801,15 +828,24 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// Update User Password
|
||||
publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx))
|
||||
|
||||
// Update User Rules
|
||||
publicUserGroupRouter.PUT("/rules", publicUser.UpdateUserRulesHandler(serverCtx))
|
||||
|
||||
// Query User Subscribe
|
||||
publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx))
|
||||
|
||||
// Get Subscribe Log
|
||||
publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx))
|
||||
|
||||
// Update User Subscribe Note
|
||||
publicUserGroupRouter.PUT("/subscribe_note", publicUser.UpdateUserSubscribeNoteHandler(serverCtx))
|
||||
|
||||
// Reset User Subscribe Token
|
||||
publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx))
|
||||
|
||||
// Unbind Device
|
||||
publicUserGroupRouter.PUT("/unbind_device", publicUser.UnbindDeviceHandler(serverCtx))
|
||||
|
||||
// Unbind OAuth
|
||||
publicUserGroupRouter.POST("/unbind_oauth", publicUser.UnbindOAuthHandler(serverCtx))
|
||||
|
||||
@@ -824,6 +860,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Verify Email
|
||||
publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx))
|
||||
|
||||
// Query Withdrawal Log
|
||||
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
|
||||
}
|
||||
|
||||
serverGroupRouter := router.Group("/v1/server")
|
||||
|
||||
@@ -36,6 +36,12 @@ func QueryServerProtocolConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Co
|
||||
|
||||
fmt.Printf("[QueryServerProtocolConfigHandler] - ShouldBindQuery request: %+v\n", req)
|
||||
|
||||
if svcCtx.Config.Node.NodeSecret != req.SecretKey {
|
||||
c.String(http.StatusUnauthorized, "Unauthorized")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
l := server.NewQueryServerProtocolConfigLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.QueryServerProtocolConfig(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
|
||||
@@ -23,6 +23,22 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
ua := c.GetHeader("User-Agent")
|
||||
req.UA = c.Request.Header.Get("User-Agent")
|
||||
req.Flag = c.Query("flag")
|
||||
if svcCtx.Config.Subscribe.PanDomain {
|
||||
domain := c.Request.Host
|
||||
domainArr := strings.Split(domain, ".")
|
||||
short, err := tool.FixedUniqueString(req.Token, 8, "")
|
||||
if err != nil {
|
||||
logger.Errorf("[SubscribeHandler] Generate short token failed: %v", err)
|
||||
c.String(http.StatusInternalServerError, "Internal Server")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if short != domainArr[0] {
|
||||
c.String(http.StatusForbidden, "Access denied")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if svcCtx.Config.Subscribe.UserAgentLimit {
|
||||
if ua == "" {
|
||||
|
||||
@@ -29,10 +29,12 @@ func NewPreviewSubscribeTemplateLogic(ctx context.Context, svcCtx *svc.ServiceCo
|
||||
}
|
||||
|
||||
func (l *PreviewSubscribeTemplateLogic) PreviewSubscribeTemplate(req *types.PreviewSubscribeTemplateRequest) (resp *types.PreviewSubscribeTemplateResponse, err error) {
|
||||
enable := true
|
||||
_, servers, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
|
||||
Page: 1,
|
||||
Size: 1000,
|
||||
Preload: true,
|
||||
Enabled: &enable,
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorf("[PreviewSubscribeTemplateLogic] FindAllServer error: %v", err.Error())
|
||||
|
||||
@@ -92,6 +92,9 @@ func (l *UpdateAuthMethodConfigLogic) UpdateGlobal(method string) {
|
||||
if method == "mobile" {
|
||||
initialize.Mobile(l.svcCtx)
|
||||
}
|
||||
if method == "device" {
|
||||
initialize.Device(l.svcCtx)
|
||||
}
|
||||
}
|
||||
|
||||
func validatePlatformConfig(platform string, cfg map[string]interface{}) (interface{}, error) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/perfect-panel/server/internal/report"
|
||||
paymentPlatform "github.com/perfect-panel/server/pkg/payment"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/payment"
|
||||
@@ -43,15 +44,31 @@ func (l *GetPaymentMethodListLogic) GetPaymentMethodList(req *types.GetPaymentMe
|
||||
Total: total,
|
||||
List: make([]types.PaymentMethodDetail, len(list)),
|
||||
}
|
||||
|
||||
// gateway mod
|
||||
|
||||
isGatewayMod := report.IsGatewayMode()
|
||||
|
||||
for i, v := range list {
|
||||
config := make(map[string]interface{})
|
||||
_ = json.Unmarshal([]byte(v.Config), &config)
|
||||
notifyUrl := ""
|
||||
|
||||
if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance {
|
||||
notifyUrl = v.Domain
|
||||
if v.Domain != "" {
|
||||
notifyUrl = v.Domain + "/v1/notify/" + v.Platform + "/" + v.Token
|
||||
// if is gateway mod, use gateway domain
|
||||
if isGatewayMod {
|
||||
notifyUrl += "/api/"
|
||||
}
|
||||
notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token
|
||||
} else {
|
||||
notifyUrl = "https://" + l.svcCtx.Config.Host + "/v1/notify/" + v.Platform + "/" + v.Token
|
||||
notifyUrl += "https://" + l.svcCtx.Config.Host
|
||||
if isGatewayMod {
|
||||
notifyUrl += "/api/v1/notify/" + v.Platform + "/" + v.Token
|
||||
} else {
|
||||
notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.List[i] = types.PaymentMethodDetail{
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package subscribe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/pkg/uuidx"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
)
|
||||
|
||||
type ResetAllSubscribeTokenLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Reset all subscribe tokens
|
||||
func NewResetAllSubscribeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetAllSubscribeTokenLogic {
|
||||
return &ResetAllSubscribeTokenLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ResetAllSubscribeTokenLogic) ResetAllSubscribeToken() (resp *types.ResetAllSubscribeTokenResponse, err error) {
|
||||
var list []*user.Subscribe
|
||||
tx := l.svcCtx.DB.WithContext(l.ctx).Begin()
|
||||
// select all active and Finished subscriptions
|
||||
if err = tx.Model(&user.Subscribe{}).Where("`status` IN ?", []int64{1, 2}).Find(&list).Error; err != nil {
|
||||
logger.Errorf("[ResetAllSubscribeToken] Failed to fetch subscribe list: %v", err.Error())
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to fetch subscribe list: %v", err.Error())
|
||||
}
|
||||
|
||||
for _, sub := range list {
|
||||
sub.Token = uuidx.SubscribeToken(strconv.FormatInt(time.Now().UnixMilli(), 10) + strconv.FormatInt(sub.Id, 10))
|
||||
sub.UUID = uuidx.NewUUID().String()
|
||||
if err = tx.Model(&user.Subscribe{}).Where("id = ?", sub.Id).Save(sub).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logger.Errorf("[ResetAllSubscribeToken] Failed to update subscribe token for ID %d: %v", sub.Id, err.Error())
|
||||
return &types.ResetAllSubscribeTokenResponse{
|
||||
Success: false,
|
||||
}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update subscribe token for ID %d: %v", sub.Id, err.Error())
|
||||
}
|
||||
}
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
logger.Errorf("[ResetAllSubscribeToken] Failed to commit transaction: %v", err.Error())
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to commit transaction: %v", err.Error())
|
||||
}
|
||||
|
||||
return &types.ResetAllSubscribeTokenResponse{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetModuleConfigLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Get Module Config
|
||||
func NewGetModuleConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetModuleConfigLogic {
|
||||
return &GetModuleConfigLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) {
|
||||
value, exists := os.LookupEnv("SECRET_KEY")
|
||||
if !exists {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
|
||||
}
|
||||
|
||||
return &types.ModuleConfig{
|
||||
Secret: value,
|
||||
ServiceName: constant.ServiceName,
|
||||
ServiceVersion: constant.Version,
|
||||
}, nil
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func NewGetTicketListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get
|
||||
}
|
||||
|
||||
func (l *GetTicketListLogic) GetTicketList(req *types.GetTicketListRequest) (resp *types.GetTicketListResponse, err error) {
|
||||
total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, int(req.Page), int(req.Size), req.UserId, req.Status, req.IssueType, req.Search)
|
||||
total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, int(req.Page), int(req.Size), req.UserId, req.Status, req.Search)
|
||||
if err != nil {
|
||||
l.Errorw("[GetTicketList] Query Database Error: ", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTicketList error: %v", err)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type QueryIPLocationLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewQueryIPLocationLogic Query IP Location
|
||||
func NewQueryIPLocationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryIPLocationLogic {
|
||||
return &QueryIPLocationLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *QueryIPLocationLogic) QueryIPLocation(req *types.QueryIPLocationRequest) (resp *types.QueryIPLocationResponse, err error) {
|
||||
if l.svcCtx.GeoIP == nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " GeoIP database not configured")
|
||||
}
|
||||
|
||||
ip := net.ParseIP(req.IP)
|
||||
record, err := l.svcCtx.GeoIP.DB.City(ip)
|
||||
if err != nil {
|
||||
l.Errorf("Failed to query IP location: %v", err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to query IP location")
|
||||
}
|
||||
|
||||
var country, region, city string
|
||||
if record.Country.Names != nil {
|
||||
country = record.Country.Names["en"]
|
||||
}
|
||||
if len(record.Subdivisions) > 0 && record.Subdivisions[0].Names != nil {
|
||||
region = record.Subdivisions[0].Names["en"]
|
||||
}
|
||||
if record.City.Names != nil {
|
||||
city = record.City.Names["en"]
|
||||
}
|
||||
|
||||
return &types.QueryIPLocationResponse{
|
||||
Country: country,
|
||||
Region: region,
|
||||
City: city,
|
||||
}, nil
|
||||
}
|
||||
@@ -40,6 +40,7 @@ func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) error {
|
||||
pwd := tool.EncodePassWord(req.Password)
|
||||
newUser := &user.User{
|
||||
Password: pwd,
|
||||
Algo: "default",
|
||||
ReferralPercentage: req.ReferralPercentage,
|
||||
OnlyFirstPurchase: &req.OnlyFirstPurchase,
|
||||
ReferCode: req.ReferCode,
|
||||
|
||||
@@ -129,6 +129,7 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
||||
return errors.Wrapf(xerr.NewErrCodeMsg(503, "Demo mode does not allow modification of the admin user password"), "UpdateUserBasicInfo failed: cannot update admin user password in demo mode")
|
||||
}
|
||||
userInfo.Password = tool.EncodePassWord(req.Password)
|
||||
userInfo.Algo = "default"
|
||||
}
|
||||
|
||||
err = l.svcCtx.UserModel.Update(l.ctx, userInfo)
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BindDeviceLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewBindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindDeviceLogic {
|
||||
return &BindDeviceLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
// BindDeviceToUser binds a device to a user
|
||||
// If the device is already bound to another user, it will disable that user and bind the device to the current user
|
||||
func (l *BindDeviceLogic) BindDeviceToUser(identifier, ip, userAgent string, currentUserId int64) error {
|
||||
if identifier == "" {
|
||||
// No device identifier provided, skip binding
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Infow("binding device to user",
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("user_id", currentUserId),
|
||||
logger.Field("ip", ip),
|
||||
)
|
||||
|
||||
// Check if device exists
|
||||
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Device not found, create new device record
|
||||
return l.createDeviceForUser(identifier, ip, userAgent, currentUserId)
|
||||
}
|
||||
l.Errorw("failed to query device",
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// Device exists, check if it's bound to current user
|
||||
if deviceInfo.UserId == currentUserId {
|
||||
// Already bound to current user, just update IP and UserAgent
|
||||
l.Infow("device already bound to current user, updating info",
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("user_id", currentUserId),
|
||||
)
|
||||
deviceInfo.Ip = ip
|
||||
deviceInfo.UserAgent = userAgent
|
||||
if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {
|
||||
l.Errorw("failed to update device",
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Device is bound to another user, need to disable old user and rebind
|
||||
l.Infow("device bound to another user, rebinding",
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("old_user_id", deviceInfo.UserId),
|
||||
logger.Field("new_user_id", currentUserId),
|
||||
)
|
||||
|
||||
return l.rebindDeviceToNewUser(deviceInfo, ip, userAgent, currentUserId)
|
||||
}
|
||||
|
||||
func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string, userId int64) error {
|
||||
l.Infow("creating new device for user",
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("user_id", userId),
|
||||
)
|
||||
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
// Create device auth method
|
||||
authMethod := &user.AuthMethods{
|
||||
UserId: userId,
|
||||
AuthType: "device",
|
||||
AuthIdentifier: identifier,
|
||||
Verified: true,
|
||||
}
|
||||
if err := db.Create(authMethod).Error; err != nil {
|
||||
l.Errorw("failed to create device auth method",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err)
|
||||
}
|
||||
|
||||
// Create device record
|
||||
deviceInfo := &user.Device{
|
||||
Ip: ip,
|
||||
UserId: userId,
|
||||
UserAgent: userAgent,
|
||||
Identifier: identifier,
|
||||
Enabled: true,
|
||||
Online: false,
|
||||
}
|
||||
if err := db.Create(deviceInfo).Error; err != nil {
|
||||
l.Errorw("failed to create device",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
l.Errorw("device creation failed",
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infow("device created successfully",
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("user_id", userId),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error {
|
||||
oldUserId := deviceInfo.UserId
|
||||
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
// Check if old user has other auth methods besides device
|
||||
var authMethods []user.AuthMethods
|
||||
if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil {
|
||||
l.Errorw("failed to query auth methods for old user",
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err)
|
||||
}
|
||||
|
||||
// Count non-device auth methods
|
||||
nonDeviceAuthCount := 0
|
||||
for _, auth := range authMethods {
|
||||
if auth.AuthType != "device" {
|
||||
nonDeviceAuthCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Only disable old user if they have no other auth methods
|
||||
if nonDeviceAuthCount == 0 {
|
||||
falseVal := false
|
||||
if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil {
|
||||
l.Errorw("failed to disable old user",
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err)
|
||||
}
|
||||
|
||||
l.Infow("disabled old user (no other auth methods)",
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
)
|
||||
} else {
|
||||
l.Infow("old user has other auth methods, not disabling",
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
logger.Field("non_device_auth_count", nonDeviceAuthCount),
|
||||
)
|
||||
}
|
||||
|
||||
// Update device auth method to new user
|
||||
if err := db.Model(&user.AuthMethods{}).
|
||||
Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier).
|
||||
Update("user_id", newUserId).Error; err != nil {
|
||||
l.Errorw("failed to update device auth method",
|
||||
logger.Field("identifier", deviceInfo.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err)
|
||||
}
|
||||
|
||||
// Update device record
|
||||
deviceInfo.UserId = newUserId
|
||||
deviceInfo.Ip = ip
|
||||
deviceInfo.UserAgent = userAgent
|
||||
deviceInfo.Enabled = true
|
||||
|
||||
if err := db.Save(deviceInfo).Error; err != nil {
|
||||
l.Errorw("failed to update device",
|
||||
logger.Field("identifier", deviceInfo.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
l.Errorw("device rebinding failed",
|
||||
logger.Field("identifier", deviceInfo.Identifier),
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
logger.Field("new_user_id", newUserId),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infow("device rebound successfully",
|
||||
logger.Field("identifier", deviceInfo.Identifier),
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
logger.Field("new_user_id", newUserId),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/model/log"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/jwt"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
"github.com/perfect-panel/server/pkg/uuidx"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DeviceLoginLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Device Login
|
||||
func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceLoginLogic {
|
||||
return &DeviceLoginLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *types.LoginResponse, err error) {
|
||||
if !l.svcCtx.Config.Device.Enable {
|
||||
return nil, xerr.NewErrMsg("Device login is disabled")
|
||||
}
|
||||
|
||||
loginStatus := false
|
||||
var userInfo *user.User
|
||||
// Record login status
|
||||
defer func() {
|
||||
if userInfo != nil && userInfo.Id != 0 {
|
||||
loginLog := log.Login{
|
||||
Method: "device",
|
||||
LoginIP: req.IP,
|
||||
UserAgent: req.UserAgent,
|
||||
Success: loginStatus,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
content, _ := loginLog.Marshal()
|
||||
if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{
|
||||
Type: log.TypeLogin.Uint8(),
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
ObjectID: userInfo.Id,
|
||||
Content: string(content),
|
||||
}); err != nil {
|
||||
l.Errorw("failed to insert login log",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("ip", req.IP),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Check if device exists by identifier
|
||||
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Device not found, create new user and device
|
||||
userInfo, err = l.registerUserAndDevice(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
l.Errorw("query device failed",
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error())
|
||||
}
|
||||
} else {
|
||||
// Device found, get user info
|
||||
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, deviceInfo.UserId)
|
||||
if err != nil {
|
||||
l.Errorw("query user failed",
|
||||
logger.Field("user_id", deviceInfo.UserId),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
|
||||
// Generate token
|
||||
token, err := jwt.NewJwtToken(
|
||||
l.svcCtx.Config.JwtAuth.AccessSecret,
|
||||
time.Now().Unix(),
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", "device"),
|
||||
)
|
||||
if err != nil {
|
||||
l.Errorw("token generate error",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
|
||||
}
|
||||
|
||||
// Store session id in redis
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
||||
l.Errorw("set session id error",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
||||
}
|
||||
|
||||
loginStatus = true
|
||||
return &types.LoginResponse{
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest) (*user.User, error) {
|
||||
l.Infow("device not found, creating new user and device",
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("ip", req.IP),
|
||||
)
|
||||
|
||||
var userInfo *user.User
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
// Create new user
|
||||
userInfo = &user.User{
|
||||
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||
}
|
||||
if err := db.Create(userInfo).Error; err != nil {
|
||||
l.Errorw("failed to create user",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err)
|
||||
}
|
||||
|
||||
// Update refer code
|
||||
userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id)
|
||||
if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil {
|
||||
l.Errorw("failed to update refer code",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err)
|
||||
}
|
||||
|
||||
// Create device auth method
|
||||
authMethod := &user.AuthMethods{
|
||||
UserId: userInfo.Id,
|
||||
AuthType: "device",
|
||||
AuthIdentifier: req.Identifier,
|
||||
Verified: true,
|
||||
}
|
||||
if err := db.Create(authMethod).Error; err != nil {
|
||||
l.Errorw("failed to create device auth method",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err)
|
||||
}
|
||||
|
||||
// Insert device record
|
||||
deviceInfo := &user.Device{
|
||||
Ip: req.IP,
|
||||
UserId: userInfo.Id,
|
||||
UserAgent: req.UserAgent,
|
||||
Identifier: req.Identifier,
|
||||
Enabled: true,
|
||||
Online: false,
|
||||
}
|
||||
if err := db.Create(deviceInfo).Error; err != nil {
|
||||
l.Errorw("failed to insert device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert device failed: %v", err)
|
||||
}
|
||||
|
||||
// Activate trial if enabled
|
||||
if l.svcCtx.Config.Register.EnableTrial {
|
||||
if err := l.activeTrial(userInfo.Id, db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
l.Errorw("device registration failed",
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Infow("device registration completed successfully",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("refer_code", userInfo.ReferCode),
|
||||
)
|
||||
|
||||
// Register log
|
||||
registerLog := log.Register{
|
||||
AuthMethod: "device",
|
||||
Identifier: req.Identifier,
|
||||
RegisterIP: req.IP,
|
||||
UserAgent: req.UserAgent,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
content, _ := registerLog.Marshal()
|
||||
|
||||
if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{
|
||||
Type: log.TypeRegister.Uint8(),
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
ObjectID: userInfo.Id,
|
||||
Content: string(content),
|
||||
}); err != nil {
|
||||
l.Errorw("failed to insert register log",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("ip", req.IP),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error {
|
||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||
if err != nil {
|
||||
l.Errorw("failed to find trial subscription template",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
expireTime := tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, startTime)
|
||||
subscribeToken := uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", userId))
|
||||
subscribeUUID := uuidx.NewUUID().String()
|
||||
|
||||
userSub := &user.Subscribe{
|
||||
UserId: userId,
|
||||
OrderId: 0,
|
||||
SubscribeId: sub.Id,
|
||||
StartTime: startTime,
|
||||
ExpireTime: expireTime,
|
||||
Traffic: sub.Traffic,
|
||||
Download: 0,
|
||||
Upload: 0,
|
||||
Token: subscribeToken,
|
||||
UUID: subscribeUUID,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if err := db.Create(userSub).Error; err != nil {
|
||||
l.Errorw("failed to insert trial subscription",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infow("trial subscription activated successfully",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("subscribe_id", sub.Id),
|
||||
logger.Field("expire_time", expireTime),
|
||||
logger.Field("traffic", sub.Traffic),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -104,9 +104,26 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
|
||||
|
||||
// Update password
|
||||
userInfo.Password = tool.EncodePassWord(req.Password)
|
||||
if err := l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil {
|
||||
userInfo.Algo = "default"
|
||||
if err = l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user info failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// Bind device to user if identifier is provided
|
||||
if req.Identifier != "" {
|
||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
||||
l.Errorw("failed to bind device to user",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
// Don't fail register if device binding fails, just log the error
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// Generate token
|
||||
@@ -116,6 +133,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@@ -98,7 +98,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
|
||||
if req.TelephoneCode == "" {
|
||||
// Verify password
|
||||
if !tool.VerifyPassWord(req.Password, userInfo.Password) {
|
||||
if !tool.MultiPasswordVerify(userInfo.Algo, userInfo.Salt, req.Password, userInfo.Password) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password")
|
||||
}
|
||||
} else {
|
||||
@@ -124,6 +124,23 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
l.svcCtx.Redis.Del(l.ctx, cacheKey)
|
||||
}
|
||||
|
||||
// Bind device to user if identifier is provided
|
||||
if req.Identifier != "" {
|
||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
||||
l.Errorw("failed to bind device to user",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
// Don't fail login if device binding fails, just log the error
|
||||
}
|
||||
}
|
||||
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// Generate token
|
||||
@@ -133,6 +150,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@@ -78,11 +78,27 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
||||
// Generate password
|
||||
pwd := tool.EncodePassWord(req.Password)
|
||||
userInfo.Password = pwd
|
||||
userInfo.Algo = "default"
|
||||
err = l.svcCtx.UserModel.Update(l.ctx, userInfo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "update user password failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// Bind device to user if identifier is provided
|
||||
if req.Identifier != "" {
|
||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
||||
l.Errorw("failed to bind device to user",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
// Don't fail register if device binding fails, just log the error
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// Generate token
|
||||
@@ -92,6 +108,7 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@@ -107,6 +107,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
||||
pwd := tool.EncodePassWord(req.Password)
|
||||
userInfo := &user.User{
|
||||
Password: pwd,
|
||||
Algo: "default",
|
||||
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||
AuthMethods: []user.AuthMethods{
|
||||
{
|
||||
@@ -138,6 +139,22 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Bind device to user if identifier is provided
|
||||
if req.Identifier != "" {
|
||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
||||
l.Errorw("failed to bind device to user",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
// Don't fail register if device binding fails, just log the error
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// Generate token
|
||||
@@ -147,6 +164,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/log"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
@@ -76,9 +77,25 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if !tool.VerifyPassWord(req.Password, userInfo.Password) {
|
||||
if !tool.MultiPasswordVerify(userInfo.Algo, userInfo.Salt, req.Password, userInfo.Password) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password")
|
||||
}
|
||||
|
||||
// Bind device to user if identifier is provided
|
||||
if req.Identifier != "" {
|
||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
||||
l.Errorw("failed to bind device to user",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
// Don't fail login if device binding fails, just log the error
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// Generate token
|
||||
@@ -88,6 +105,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@@ -90,6 +90,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
||||
pwd := tool.EncodePassWord(req.Password)
|
||||
userInfo := &user.User{
|
||||
Password: pwd,
|
||||
Algo: "default",
|
||||
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||
}
|
||||
if referer != nil {
|
||||
@@ -125,6 +126,21 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// Bind device to user if identifier is provided
|
||||
if req.Identifier != "" {
|
||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
||||
l.Errorw("failed to bind device to user",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
// Don't fail register if device binding fails, just log the error
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// Generate token
|
||||
@@ -134,6 +150,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@@ -2,6 +2,7 @@ package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
@@ -67,6 +68,10 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes
|
||||
for _, method := range authMethods {
|
||||
if *method.Enabled {
|
||||
methods = append(methods, method.Method)
|
||||
if method.Method == "device" {
|
||||
_ = json.Unmarshal([]byte(method.Config), &resp.Auth.Device)
|
||||
resp.Auth.Device.Enable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.OAuthMethods = methods
|
||||
|
||||
@@ -3,6 +3,11 @@ package common
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
@@ -63,14 +68,52 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) {
|
||||
l.Logger.Error("[GetStatLogic] get server_addr failed: ", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server_addr failed: %v", err.Error())
|
||||
}
|
||||
|
||||
country := 0
|
||||
|
||||
protocolDict := make(map[string]struct{})
|
||||
type apireq struct {
|
||||
query string
|
||||
fields string
|
||||
}
|
||||
type apiret struct {
|
||||
CountryCode string `json:"countryCode"`
|
||||
}
|
||||
//map as dict
|
||||
type void struct{}
|
||||
var v void
|
||||
country := make(map[string]void)
|
||||
for c := range slices.Chunk(nodeaddr, 100) {
|
||||
var batchreq []apireq
|
||||
for _, addr := range c {
|
||||
isAddr := net.ParseIP(addr)
|
||||
if isAddr == nil {
|
||||
ip, err := net.LookupIP(addr)
|
||||
if err == nil && len(ip) > 0 {
|
||||
batchreq = append(batchreq, apireq{query: ip[0].String(), fields: "countryCode"})
|
||||
}
|
||||
} else {
|
||||
batchreq = append(batchreq, apireq{query: addr, fields: "countryCode"})
|
||||
}
|
||||
}
|
||||
req, _ := json.Marshal(batchreq)
|
||||
ret, err := http.Post("http://ip-api.com/batch", "application/json", strings.NewReader(string(req)))
|
||||
if err == nil {
|
||||
retBytes, err := io.ReadAll(ret.Body)
|
||||
if err == nil {
|
||||
var retStruct []apiret
|
||||
err := json.Unmarshal(retBytes, &retStruct)
|
||||
if err == nil {
|
||||
for _, dat := range retStruct {
|
||||
if dat.CountryCode != "" {
|
||||
country[dat.CountryCode] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protocolDict := make(map[string]void)
|
||||
var protocol []string
|
||||
l.svcCtx.DB.Model(&server.Server{}).Where("enable = true").Pluck("protocol", &protocol)
|
||||
for _, p := range protocol {
|
||||
protocolDict[p] = struct{}{}
|
||||
protocolDict[p] = v
|
||||
}
|
||||
protocol = nil
|
||||
for p := range protocolDict {
|
||||
@@ -79,7 +122,7 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) {
|
||||
resp = &types.GetStatResponse{
|
||||
User: u,
|
||||
Node: n,
|
||||
Country: int64(country),
|
||||
Country: int64(len(country)),
|
||||
Protocol: protocol,
|
||||
OnlineDevice: l.svcCtx.DeviceManager.GetOnlineDeviceCount(),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
)
|
||||
|
||||
type HeartbeatLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewHeartbeatLogic Heartbeat
|
||||
func NewHeartbeatLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HeartbeatLogic {
|
||||
return &HeartbeatLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *HeartbeatLogic) Heartbeat() (resp *types.HeartbeatResponse, err error) {
|
||||
return &types.HeartbeatResponse{
|
||||
Status: true,
|
||||
Message: "service is alive",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}, nil
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
||||
return err
|
||||
}
|
||||
// Verify sign
|
||||
client := epay.NewClient(config.Pid, config.Url, config.Key)
|
||||
client := epay.NewClient(config.Pid, config.Url, config.Key, config.Type)
|
||||
if !client.VerifySign(urlParamsToMap(l.ctx.Request.URL.RawQuery)) && !l.svcCtx.Config.Debug {
|
||||
l.Logger.Error("[EPayNotify] Verify sign failed")
|
||||
return nil
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/log"
|
||||
"github.com/perfect-panel/server/internal/report"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
|
||||
paymentPlatform "github.com/perfect-panel/server/pkg/payment"
|
||||
@@ -267,7 +268,7 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
|
||||
}
|
||||
// Initialize EPay client with merchant credentials
|
||||
client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key)
|
||||
client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key, epayConfig.Type)
|
||||
|
||||
// Convert order amount to CNY using current exchange rate
|
||||
amount, err := l.queryExchangeRate("CNY", info.Amount)
|
||||
@@ -275,16 +276,29 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
|
||||
return "", err
|
||||
}
|
||||
|
||||
// gateway mod
|
||||
|
||||
isGatewayMod := report.IsGatewayMode()
|
||||
|
||||
// Build notification URL for payment status callbacks
|
||||
notifyUrl := ""
|
||||
if config.Domain != "" {
|
||||
notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||
notifyUrl = config.Domain
|
||||
if isGatewayMod {
|
||||
notifyUrl += "/api/"
|
||||
}
|
||||
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||
} else {
|
||||
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
|
||||
if !ok {
|
||||
host = l.svcCtx.Config.Host
|
||||
}
|
||||
notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||
|
||||
notifyUrl = "https://" + host
|
||||
if isGatewayMod {
|
||||
notifyUrl += "/api"
|
||||
}
|
||||
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||
}
|
||||
|
||||
// Create payment URL for user redirection
|
||||
@@ -309,7 +323,7 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
|
||||
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
|
||||
}
|
||||
// Initialize EPay client with merchant credentials
|
||||
client := epay.NewClient(epayConfig.AccountID, epayConfig.Endpoint, epayConfig.SecretKey)
|
||||
client := epay.NewClient(epayConfig.AccountID, epayConfig.Endpoint, epayConfig.SecretKey, epayConfig.Type)
|
||||
|
||||
// Convert order amount to CNY using current exchange rate
|
||||
amount, err := l.queryExchangeRate("CNY", info.Amount)
|
||||
@@ -317,18 +331,29 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
|
||||
return "", err
|
||||
}
|
||||
|
||||
// gateway mod
|
||||
isGatewayMod := report.IsGatewayMode()
|
||||
|
||||
// Build notification URL for payment status callbacks
|
||||
notifyUrl := ""
|
||||
if config.Domain != "" {
|
||||
notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||
notifyUrl = config.Domain
|
||||
if isGatewayMod {
|
||||
notifyUrl += "/api/"
|
||||
}
|
||||
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||
} else {
|
||||
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
|
||||
if !ok {
|
||||
host = l.svcCtx.Config.Host
|
||||
}
|
||||
notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||
}
|
||||
|
||||
notifyUrl = "https://" + host
|
||||
if isGatewayMod {
|
||||
notifyUrl += "/api"
|
||||
}
|
||||
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||
}
|
||||
// Create payment URL for user redirection
|
||||
url := client.CreatePayUrl(epay.Order{
|
||||
Name: l.svcCtx.Config.Site.SiteName,
|
||||
@@ -347,6 +372,11 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount
|
||||
// Convert cents to decimal amount
|
||||
amount = float64(src) / float64(100)
|
||||
|
||||
if l.svcCtx.ExchangeRate != 0 && to == "CNY" {
|
||||
amount = amount * l.svcCtx.ExchangeRate
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
// Retrieve system currency configuration
|
||||
currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -83,6 +83,12 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
|
||||
if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used")
|
||||
}
|
||||
// Check expiration time
|
||||
expireTime := time.Unix(couponInfo.ExpireTime, 0)
|
||||
if time.Now().After(expireTime) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponExpired), "coupon expired")
|
||||
}
|
||||
|
||||
couponSub := tool.StringToInt64Slice(couponInfo.Subscribe)
|
||||
if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match")
|
||||
|
||||
@@ -58,17 +58,12 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeLi
|
||||
}
|
||||
list[i] = sub
|
||||
// 通过服务组查询关联的节点数量
|
||||
if item.ServerGroup != "" {
|
||||
groupIds := tool.StringToInt64Slice(item.ServerGroup)
|
||||
servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, groupIds)
|
||||
if err != nil {
|
||||
l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error()))
|
||||
sub.ServerCount = 0
|
||||
} else {
|
||||
sub.ServerCount = int64(len(servers))
|
||||
}
|
||||
} else {
|
||||
servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, tool.StringToInt64Slice(sub.ServerGroup))
|
||||
if err != nil {
|
||||
l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error()))
|
||||
sub.ServerCount = 0
|
||||
} else {
|
||||
sub.ServerCount = int64(len(servers))
|
||||
}
|
||||
list[i] = sub
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package subscribe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/node"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type QueryUserSubscribeNodeListLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Get user subscribe node info
|
||||
func NewQueryUserSubscribeNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserSubscribeNodeListLogic {
|
||||
return &QueryUserSubscribeNodeListLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *types.QueryUserSubscribeNodeListResponse, err error) {
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
logger.Error("current user is not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 1, 2)
|
||||
if err != nil {
|
||||
logger.Errorw("failed to query user subscribe", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "DB_ERROR")
|
||||
}
|
||||
|
||||
resp = &types.QueryUserSubscribeNodeListResponse{}
|
||||
for _, us := range userSubscribes {
|
||||
userSubscribe, err := l.getUserSubscribe(us.Token)
|
||||
if err != nil {
|
||||
l.Errorw("[SubscribeLogic] Get user subscribe failed", logger.Field("error", err.Error()), logger.Field("token", userSubscribe.Token))
|
||||
return nil, err
|
||||
}
|
||||
nodes, err := l.getServers(userSubscribe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userSubscribeInfo := types.UserSubscribeInfo{
|
||||
Id: userSubscribe.Id,
|
||||
Nodes: nodes,
|
||||
Traffic: userSubscribe.Traffic,
|
||||
Upload: userSubscribe.Upload,
|
||||
Download: userSubscribe.Download,
|
||||
Token: userSubscribe.Token,
|
||||
UserId: userSubscribe.UserId,
|
||||
OrderId: userSubscribe.OrderId,
|
||||
SubscribeId: userSubscribe.SubscribeId,
|
||||
StartTime: userSubscribe.StartTime.Unix(),
|
||||
ExpireTime: userSubscribe.ExpireTime.Unix(),
|
||||
Status: userSubscribe.Status,
|
||||
CreatedAt: userSubscribe.CreatedAt.Unix(),
|
||||
UpdatedAt: userSubscribe.UpdatedAt.Unix(),
|
||||
}
|
||||
|
||||
if userSubscribe.FinishedAt != nil {
|
||||
userSubscribeInfo.FinishedAt = userSubscribe.FinishedAt.Unix()
|
||||
}
|
||||
|
||||
if l.svcCtx.Config.Register.EnableTrial && l.svcCtx.Config.Register.TrialSubscribe == userSubscribe.SubscribeId {
|
||||
userSubscribeInfo.IsTryOut = true
|
||||
}
|
||||
|
||||
resp.List = append(resp.List, userSubscribeInfo)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (userSubscribeNodes []*types.UserSubscribeNodeInfo, err error) {
|
||||
userSubscribeNodes = make([]*types.UserSubscribeNodeInfo, 0)
|
||||
if l.isSubscriptionExpired(userSub) {
|
||||
return l.createExpiredServers(), nil
|
||||
}
|
||||
|
||||
subDetails, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSub.SubscribeId)
|
||||
if err != nil {
|
||||
l.Errorw("[Generate Subscribe]find subscribe details error: %v", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error())
|
||||
}
|
||||
nodeIds := tool.StringToInt64Slice(subDetails.Nodes)
|
||||
tags := strings.Split(subDetails.NodeTags, ",")
|
||||
|
||||
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags)
|
||||
|
||||
enable := true
|
||||
|
||||
_, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
|
||||
Page: 0,
|
||||
Size: 1000,
|
||||
NodeId: nodeIds,
|
||||
Enabled: &enable, // Only get enabled nodes
|
||||
})
|
||||
|
||||
if len(nodes) > 0 {
|
||||
var serverMapIds = make(map[int64]*node.Server)
|
||||
for _, n := range nodes {
|
||||
serverMapIds[n.ServerId] = nil
|
||||
}
|
||||
var serverIds []int64
|
||||
for k := range serverMapIds {
|
||||
serverIds = append(serverIds, k)
|
||||
}
|
||||
|
||||
servers, err := l.svcCtx.NodeModel.QueryServerList(l.ctx, serverIds)
|
||||
if err != nil {
|
||||
l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error())
|
||||
}
|
||||
|
||||
for _, s := range servers {
|
||||
serverMapIds[s.Id] = s
|
||||
}
|
||||
|
||||
for _, n := range nodes {
|
||||
server := serverMapIds[n.ServerId]
|
||||
if server == nil {
|
||||
continue
|
||||
}
|
||||
userSubscribeNode := &types.UserSubscribeNodeInfo{
|
||||
Id: n.Id,
|
||||
Name: n.Name,
|
||||
Uuid: userSub.UUID,
|
||||
Protocol: n.Protocol,
|
||||
Port: n.Port,
|
||||
Address: n.Address,
|
||||
Tags: strings.Split(n.Tags, ","),
|
||||
Country: server.Country,
|
||||
City: server.City,
|
||||
CreatedAt: n.CreatedAt.Unix(),
|
||||
}
|
||||
userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode)
|
||||
}
|
||||
}
|
||||
|
||||
l.Debugf("[Query Subscribe]found servers: %v", len(nodes))
|
||||
|
||||
if err != nil {
|
||||
l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error())
|
||||
}
|
||||
logger.Debugf("[Generate Subscribe]found servers: %v", len(nodes))
|
||||
return userSubscribeNodes, nil
|
||||
}
|
||||
|
||||
func (l *QueryUserSubscribeNodeListLogic) isSubscriptionExpired(userSub *user.Subscribe) bool {
|
||||
return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0
|
||||
}
|
||||
|
||||
func (l *QueryUserSubscribeNodeListLogic) createExpiredServers() []*types.UserSubscribeNodeInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *QueryUserSubscribeNodeListLogic) getFirstHostLine() string {
|
||||
host := l.svcCtx.Config.Host
|
||||
lines := strings.Split(host, "\n")
|
||||
if len(lines) > 0 {
|
||||
return lines[0]
|
||||
}
|
||||
return host
|
||||
}
|
||||
func (l *QueryUserSubscribeNodeListLogic) getUserSubscribe(token string) (*user.Subscribe, error) {
|
||||
userSub, err := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token)
|
||||
if err != nil {
|
||||
l.Infow("[Generate Subscribe]find subscribe error: %v", logger.Field("error", err.Error()), logger.Field("token", token))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
|
||||
}
|
||||
|
||||
// Ignore expiration check
|
||||
//if userSub.Status > 1 {
|
||||
// l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token))
|
||||
// return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available")
|
||||
//}
|
||||
|
||||
return userSub, nil
|
||||
}
|
||||
@@ -41,7 +41,6 @@ func (l *CreateUserTicketLogic) CreateUserTicket(req *types.CreateUserTicketRequ
|
||||
Description: req.Description,
|
||||
UserId: u.Id,
|
||||
Status: ticket.Pending,
|
||||
IssueType: req.IssueType,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert ticket error: %v", err.Error())
|
||||
|
||||
@@ -36,7 +36,7 @@ func (l *GetUserTicketListLogic) GetUserTicketList(req *types.GetUserTicketListR
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
l.Logger.Debugf("Current user: %v", u.Id)
|
||||
total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, req.Page, req.Size, u.Id, req.Status, req.IssueType, req.Search)
|
||||
total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, req.Page, req.Size, u.Id, req.Status, req.Search)
|
||||
if err != nil {
|
||||
l.Errorw("[GetUserTicketListLogic] Database Error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTicketList error: %v", err)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/log"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type CommissionWithdrawLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Commission Withdraw
|
||||
func NewCommissionWithdrawLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CommissionWithdrawLogic {
|
||||
return &CommissionWithdrawLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdrawRequest) (resp *types.WithdrawalLog, err error) {
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
logger.Error("current user is not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
if u.Commission < req.Amount {
|
||||
logger.Errorf("User %d has insufficient commission balance: %.2f, requested: %.2f", u.Id, float64(u.Commission)/100, float64(req.Amount)/100)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserCommissionNotEnough), "User %d has insufficient commission balance", u.Id)
|
||||
}
|
||||
|
||||
tx := l.svcCtx.DB.WithContext(l.ctx).Begin()
|
||||
|
||||
// update user commission balance
|
||||
u.Commission -= req.Amount
|
||||
if err = l.svcCtx.UserModel.Update(l.ctx, u, tx); err != nil {
|
||||
tx.Rollback()
|
||||
l.Errorf("Failed to update user %d commission balance: %v", u.Id, err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update user %d commission balance: %v", u.Id, err)
|
||||
}
|
||||
|
||||
// create withdrawal log
|
||||
logInfo := log.Commission{
|
||||
Type: log.CommissionTypeConvertBalance,
|
||||
Amount: req.Amount,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
b, err := logInfo.Marshal()
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
l.Errorf("Failed to marshal commission log for user %d: %v", u.Id, err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to marshal commission log for user %d: %v", u.Id, err)
|
||||
}
|
||||
|
||||
err = tx.Model(log.SystemLog{}).Create(&log.SystemLog{
|
||||
Type: log.TypeCommission.Uint8(),
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
ObjectID: u.Id,
|
||||
Content: string(b),
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
l.Errorf("Failed to create commission log for user %d: %v", u.Id, err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create commission log for user %d: %v", u.Id, err)
|
||||
}
|
||||
|
||||
err = tx.Model(&user.Withdrawal{}).Create(&user.Withdrawal{
|
||||
UserId: u.Id,
|
||||
Amount: req.Amount,
|
||||
Content: req.Content,
|
||||
Status: 0,
|
||||
Reason: "",
|
||||
}).Error
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
l.Errorf("Failed to create withdrawal log for user %d: %v", u.Id, err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create withdrawal log for user %d: %v", u.Id, err)
|
||||
}
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
l.Errorf("Transaction commit failed for user %d withdrawal: %v", u.Id, err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Transaction commit failed for user %d withdrawal: %v", u.Id, err)
|
||||
}
|
||||
|
||||
return &types.WithdrawalLog{
|
||||
UserId: u.Id,
|
||||
Amount: req.Amount,
|
||||
Content: req.Content,
|
||||
Status: 0,
|
||||
Reason: "",
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
)
|
||||
|
||||
type GetDeviceListLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Get Device List
|
||||
func NewGetDeviceListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetDeviceListLogic {
|
||||
return &GetDeviceListLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse, err error) {
|
||||
userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
list, count, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, userInfo.Id)
|
||||
userRespList := make([]types.UserDevice, 0)
|
||||
tool.DeepCopy(&userRespList, list)
|
||||
resp = &types.GetDeviceListResponse{
|
||||
Total: count,
|
||||
List: userRespList,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func NewQueryUserBalanceLogLogic(ctx context.Context, svcCtx *svc.ServiceContext
|
||||
}
|
||||
}
|
||||
|
||||
func (l *QueryUserBalanceLogLogic) QueryUserBalanceLog(req *types.QueryUserBalanceLogListRequest) (resp *types.QueryUserBalanceLogListResponse, err error) {
|
||||
func (l *QueryUserBalanceLogLogic) QueryUserBalanceLog() (resp *types.QueryUserBalanceLogListResponse, err error) {
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
logger.Error("current user is not found in context")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user